@houwert/conductor 0.9.0 → 0.11.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/README.md CHANGED
@@ -71,7 +71,7 @@ Claude learns every available command, how to coordinate across devices, and how
71
71
  |---|---|
72
72
  | App lifecycle | `launch-app`, `stop-app`, `clear-state`, `uninstall-app`, `install-app`, `foreground-app`, `copy-app` |
73
73
  | Interaction | `tap-on`, `input-text`, `scroll`, `scroll-until-visible`, `swipe`, `press-key`, `erase-text`, `hide-keyboard` |
74
- | Inspection | `inspect`, `focused`, `take-screenshot`, `list-apps` |
74
+ | Inspection | `inspect`, `focused`, `take-screenshot`, `capture-ui`, `list-apps` |
75
75
  | Assertions | `assert-visible`, `assert-not-visible` |
76
76
  | Navigation | `open-link`, `back` |
77
77
  | Flows | `run-flow`, `run-flow-inline`, `run-parallel` |
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HELP = void 0;
7
+ exports.captureUI = captureUI;
8
+ exports.HELP = ` capture-ui [--output <path>] Capture screenshot + hierarchy + a11y snapshot (for Argus UI panel)`;
9
+ const promises_1 = __importDefault(require("fs/promises"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const runner_js_1 = require("../runner.js");
12
+ const output_js_1 = require("../output.js");
13
+ const ios_js_1 = require("../drivers/ios.js");
14
+ const android_js_1 = require("../drivers/android.js");
15
+ const web_js_1 = require("../drivers/web.js");
16
+ const a11y_js_1 = require("../drivers/a11y.js");
17
+ async function captureUI(outputPath, opts = {}, sessionName = 'default') {
18
+ try {
19
+ const driver = await (0, runner_js_1.getDriver)(sessionName);
20
+ const capturedAt = new Date().toISOString();
21
+ let platform;
22
+ let width = 0;
23
+ let height = 0;
24
+ let hierarchy;
25
+ let a11ySnapshot;
26
+ let screenshotBuf;
27
+ if (driver instanceof ios_js_1.IOSDriver) {
28
+ platform = driver.platform; // 'ios' | 'tvos'
29
+ const [info, vh, shot] = await Promise.all([
30
+ driver.deviceInfo(),
31
+ driver.viewHierarchy(false),
32
+ driver.screenshot(),
33
+ ]);
34
+ width = info.widthPoints;
35
+ height = info.heightPoints;
36
+ const built = (0, a11y_js_1.buildIOSA11y)(vh.axElement);
37
+ hierarchy = { axElement: built.hierarchy, depth: vh.depth };
38
+ a11ySnapshot = built.a11ySnapshot;
39
+ screenshotBuf = shot;
40
+ }
41
+ else if (driver instanceof web_js_1.WebDriver) {
42
+ platform = 'web';
43
+ const [info, vh, shot] = await Promise.all([
44
+ driver.deviceInfo(),
45
+ driver.viewHierarchy(),
46
+ driver.screenshot(),
47
+ ]);
48
+ width = info.widthPixels;
49
+ height = info.heightPixels;
50
+ const built = (0, a11y_js_1.buildWebA11y)(vh);
51
+ hierarchy = { ...vh, elements: built.hierarchy };
52
+ a11ySnapshot = built.a11ySnapshot;
53
+ screenshotBuf = shot;
54
+ }
55
+ else if (driver instanceof android_js_1.AndroidDriver) {
56
+ platform = 'android';
57
+ const [info, xml, shot] = await Promise.all([
58
+ driver.deviceInfo(),
59
+ driver.viewHierarchy(),
60
+ driver.screenshot(),
61
+ ]);
62
+ width = info.widthPixels;
63
+ height = info.heightPixels;
64
+ const built = (0, a11y_js_1.buildAndroidA11y)(xml);
65
+ hierarchy = { xml, elements: built.hierarchy };
66
+ a11ySnapshot = built.a11ySnapshot;
67
+ screenshotBuf = shot;
68
+ }
69
+ else {
70
+ throw new Error('Unknown driver type');
71
+ }
72
+ const bundle = {
73
+ version: 1,
74
+ capturedAt,
75
+ device: {
76
+ platform,
77
+ deviceId: sessionName,
78
+ width,
79
+ height,
80
+ },
81
+ screenshot: {
82
+ kind: 'composite',
83
+ encoding: 'png',
84
+ data: screenshotBuf.toString('base64'),
85
+ },
86
+ hierarchy,
87
+ a11ySnapshot,
88
+ capabilities: { perViewPixels: false, depthData: false },
89
+ };
90
+ const json = JSON.stringify(bundle);
91
+ if (outputPath) {
92
+ const resolved = path_1.default.resolve(outputPath);
93
+ await promises_1.default.writeFile(resolved, json);
94
+ if (opts.json) {
95
+ console.log(JSON.stringify({
96
+ status: 'ok',
97
+ path: resolved,
98
+ bytes: Buffer.byteLength(json, 'utf-8'),
99
+ }));
100
+ }
101
+ else {
102
+ (0, output_js_1.printSuccess)(`capture-ui saved to ${resolved}`, opts);
103
+ }
104
+ }
105
+ else {
106
+ // Stdout: raw JSON bundle (no pretty-printing — screenshot is huge).
107
+ process.stdout.write(json);
108
+ if (!opts.json)
109
+ process.stdout.write('\n');
110
+ }
111
+ return 0;
112
+ }
113
+ catch (err) {
114
+ const msg = err instanceof Error ? err.message : String(err);
115
+ (0, output_js_1.printError)(`capture-ui — failed\n${msg}`, opts);
116
+ return 1;
117
+ }
118
+ }
@@ -67,7 +67,8 @@ ASSERTIONS
67
67
 
68
68
  SCREENSHOTS & INSPECTION
69
69
  take-screenshot [--output <path>] Take screenshot (default: ./screenshot-<ts>.png)
70
- inspect Print UI hierarchy
70
+ inspect [--dump] Print UI hierarchy (--dump: a11y-enriched JSON)
71
+ capture-ui [--output <path>] Screenshot + hierarchy + a11y snapshot bundle (Argus)
71
72
 
72
73
  FLOW EXECUTION
73
74
  run-flow <file> [--device <id>] Run a Maestro YAML flow file
@@ -9,6 +9,7 @@ const ios_js_1 = require("../drivers/ios.js");
9
9
  const android_js_1 = require("../drivers/android.js");
10
10
  const web_js_1 = require("../drivers/web.js");
11
11
  const element_resolver_js_1 = require("../drivers/element-resolver.js");
12
+ const a11y_js_1 = require("../drivers/a11y.js");
12
13
  async function inspect(opts = {}, sessionName = 'default', inspectOpts = {}) {
13
14
  try {
14
15
  const driver = await (0, runner_js_1.getDriver)(sessionName);
@@ -16,13 +17,20 @@ async function inspect(opts = {}, sessionName = 'default', inspectOpts = {}) {
16
17
  let raw;
17
18
  if (driver instanceof ios_js_1.IOSDriver) {
18
19
  const hierarchy = await driver.viewHierarchy(false);
19
- raw = JSON.stringify(hierarchy, null, 2);
20
+ // Augment each node with a11y fields (traits, accessibilityOrder,
21
+ // isAccessibilityElement, announcement). All existing fields are preserved.
22
+ const built = (0, a11y_js_1.buildIOSA11y)(hierarchy.axElement);
23
+ raw = JSON.stringify({ axElement: built.hierarchy, depth: hierarchy.depth }, null, 2);
20
24
  }
21
25
  else if (driver instanceof web_js_1.WebDriver) {
22
- raw = JSON.stringify(await driver.viewHierarchy(), null, 2);
26
+ const vh = await driver.viewHierarchy();
27
+ const built = (0, a11y_js_1.buildWebA11y)(vh);
28
+ raw = JSON.stringify({ ...vh, elements: built.hierarchy }, null, 2);
23
29
  }
24
30
  else if (driver instanceof android_js_1.AndroidDriver) {
25
- raw = await driver.viewHierarchy();
31
+ const xml = await driver.viewHierarchy();
32
+ const built = (0, a11y_js_1.buildAndroidA11y)(xml);
33
+ raw = JSON.stringify(built.hierarchy, null, 2);
26
34
  }
27
35
  else {
28
36
  throw new Error('Unknown driver type');
@@ -0,0 +1,416 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.composeIOSAnnouncement = composeIOSAnnouncement;
4
+ exports.buildIOSA11y = buildIOSA11y;
5
+ exports.composeAndroidAnnouncement = composeAndroidAnnouncement;
6
+ exports.buildAndroidA11y = buildAndroidA11y;
7
+ exports.composeWebAnnouncement = composeWebAnnouncement;
8
+ exports.buildWebA11y = buildWebA11y;
9
+ const element_resolver_js_1 = require("./element-resolver.js");
10
+ // ── iOS ──────────────────────────────────────────────────────────────────────
11
+ /** XCUIElementType raw values → trait/role strings. Incomplete by design: only
12
+ * the element types that map to user-facing roles are listed; others fall back
13
+ * to an empty traits array and an empty role string. */
14
+ const IOS_TYPE_TO_TRAIT = {
15
+ 3: 'application',
16
+ 4: 'window',
17
+ 8: 'image',
18
+ 9: 'button',
19
+ 23: 'adjustable',
20
+ 40: 'switch',
21
+ 48: 'staticText',
22
+ 49: 'textField',
23
+ 50: 'secureTextField',
24
+ 54: 'link',
25
+ 70: 'table',
26
+ 73: 'picker',
27
+ 74: 'pickerWheel',
28
+ 75: 'cell',
29
+ 90: 'stepper',
30
+ 93: 'searchField',
31
+ };
32
+ function iosTraitsFor(node) {
33
+ const out = [];
34
+ const base = IOS_TYPE_TO_TRAIT[node.elementType];
35
+ if (base)
36
+ out.push(base);
37
+ if (node.selected)
38
+ out.push('selected');
39
+ if (!node.enabled)
40
+ out.push('disabled');
41
+ if (node.hasFocus)
42
+ out.push('focused');
43
+ return out;
44
+ }
45
+ function isIOSA11yElement(node) {
46
+ // VoiceOver considers an element "accessible" if it has a label or a known interactive type.
47
+ // AXElement doesn't expose isAccessibilityElement directly; this is a documented approximation.
48
+ const hasText = !!(node.label || node.title || node.value || node.placeholderValue);
49
+ const hasTrait = !!IOS_TYPE_TO_TRAIT[node.elementType];
50
+ const hasFrame = node.frame.Width > 0 && node.frame.Height > 0;
51
+ return hasFrame && (hasText || hasTrait);
52
+ }
53
+ /** iOS announcement: `[label], [traits], [value], [hint]` — commas between parts. */
54
+ function composeIOSAnnouncement(label, traits, value, hint) {
55
+ const parts = [];
56
+ if (label)
57
+ parts.push(label);
58
+ const announceableTraits = traits.filter((t) => t !== 'staticText' && t !== 'window' && t !== 'application');
59
+ if (announceableTraits.length)
60
+ parts.push(announceableTraits.join(', '));
61
+ if (value && value !== label)
62
+ parts.push(value);
63
+ if (hint)
64
+ parts.push(hint);
65
+ return parts.join(', ');
66
+ }
67
+ function buildIOSA11y(root) {
68
+ const snapshot = [];
69
+ let order = 0;
70
+ function walk(node, path) {
71
+ const traits = iosTraitsFor(node);
72
+ const isA11y = isIOSA11yElement(node);
73
+ const label = node.label || node.title || '';
74
+ const value = node.value || node.placeholderValue || '';
75
+ const hint = node.hint || '';
76
+ const announcement = isA11y ? composeIOSAnnouncement(label, traits, value, hint) : '';
77
+ let accessibilityOrder = null;
78
+ if (isA11y) {
79
+ accessibilityOrder = order++;
80
+ snapshot.push({
81
+ nodeId: path,
82
+ order: accessibilityOrder,
83
+ frame: {
84
+ x: node.frame.X,
85
+ y: node.frame.Y,
86
+ w: node.frame.Width,
87
+ h: node.frame.Height,
88
+ },
89
+ label,
90
+ hint,
91
+ role: traits[0] ?? '',
92
+ traits,
93
+ announcement,
94
+ value,
95
+ state: {
96
+ enabled: node.enabled,
97
+ selected: node.selected,
98
+ focused: node.hasFocus,
99
+ },
100
+ });
101
+ }
102
+ const children = (node.children ?? []).map((c, i) => walk(c, path === '' ? String(i) : `${path}.${i}`));
103
+ const enriched = {
104
+ ...node,
105
+ nodeId: path,
106
+ accessibilityOrder,
107
+ traits,
108
+ isAccessibilityElement: isA11y,
109
+ accessibilityIdentifier: node.identifier,
110
+ accessibilityLabel: label || undefined,
111
+ accessibilityHint: hint || undefined,
112
+ accessibilityValue: value || undefined,
113
+ announcement: announcement || undefined,
114
+ children: children.length ? children : undefined,
115
+ };
116
+ return enriched;
117
+ }
118
+ const hierarchy = walk(root, '0');
119
+ return { hierarchy, a11ySnapshot: snapshot };
120
+ }
121
+ // Android className → semantic role. Matches AccessibilityNodeInfoCompat defaults.
122
+ const ANDROID_CLASS_ROLE = [
123
+ [/Button$/, 'button'],
124
+ [/ImageButton$/, 'button'],
125
+ [/EditText$/, 'textField'],
126
+ [/CheckBox$/, 'checkbox'],
127
+ [/Switch$/, 'switch'],
128
+ [/ToggleButton$/, 'switch'],
129
+ [/RadioButton$/, 'radio'],
130
+ [/Spinner$/, 'dropdown'],
131
+ [/SeekBar$/, 'adjustable'],
132
+ [/ProgressBar$/, 'progressIndicator'],
133
+ [/TextView$/, 'staticText'],
134
+ [/ImageView$/, 'image'],
135
+ [/WebView$/, 'webView'],
136
+ ];
137
+ function androidRoleFor(className) {
138
+ for (const [re, role] of ANDROID_CLASS_ROLE) {
139
+ if (re.test(className))
140
+ return role;
141
+ }
142
+ return '';
143
+ }
144
+ /** Android announcement: `[contentDescription || text], [role], [state], [hint]`. */
145
+ function composeAndroidAnnouncement(text, contentDescription, role, stateParts, hint) {
146
+ const parts = [];
147
+ const spoken = contentDescription || text;
148
+ if (spoken)
149
+ parts.push(spoken);
150
+ if (role)
151
+ parts.push(role);
152
+ if (stateParts.length)
153
+ parts.push(stateParts.join(', '));
154
+ if (hint)
155
+ parts.push(hint);
156
+ return parts.join(', ');
157
+ }
158
+ /**
159
+ * Parse Android XML while preserving nesting — parseAndroidHierarchy returns a flat list,
160
+ * but we need tree structure for stable nodeId paths. We re-walk the XML here with a small
161
+ * state machine (open-tag depth) to reconstruct parent/child relationships.
162
+ */
163
+ function parseAndroidTree(xml) {
164
+ const flat = (0, element_resolver_js_1.parseAndroidHierarchy)(xml);
165
+ // Parent index per node, computed from ordered XML scan.
166
+ const openStack = [];
167
+ const parentOf = new Array(flat.length).fill(-1);
168
+ // Regex that walks open/close tags in order.
169
+ const tokenRe = /<node\b[^>]*?(\/?)>|<\/node>/g;
170
+ let idx = 0;
171
+ let m;
172
+ while ((m = tokenRe.exec(xml)) !== null) {
173
+ const tag = m[0];
174
+ if (tag === '</node>') {
175
+ openStack.pop();
176
+ continue;
177
+ }
178
+ const selfClosing = m[1] === '/';
179
+ // Does this node have a valid bounds? Only those appear in `flat`.
180
+ const boundsMatch = /\bbounds="(\[[^"]+\])"/.exec(tag);
181
+ const hasValidBounds = boundsMatch &&
182
+ /\[(\d+),(\d+)]\[(\d+),(\d+)]/.test(boundsMatch[1]) &&
183
+ (() => {
184
+ const [, x1, y1, x2, y2] = /\[(\d+),(\d+)]\[(\d+),(\d+)]/.exec(boundsMatch[1]);
185
+ return +x2 - +x1 > 0 && +y2 - +y1 > 0;
186
+ })();
187
+ if (hasValidBounds) {
188
+ parentOf[idx] = openStack.length ? openStack[openStack.length - 1] : -1;
189
+ if (!selfClosing)
190
+ openStack.push(idx);
191
+ idx++;
192
+ }
193
+ else if (!selfClosing) {
194
+ // Non-visible container: push a sentinel so close tags still pop correctly.
195
+ openStack.push(-1);
196
+ }
197
+ }
198
+ const childrenOf = new Map();
199
+ const roots = [];
200
+ parentOf.forEach((p, i) => {
201
+ if (p === -1)
202
+ roots.push(i);
203
+ else {
204
+ const arr = childrenOf.get(p) ?? [];
205
+ arr.push(i);
206
+ childrenOf.set(p, arr);
207
+ }
208
+ });
209
+ const result = flat.map((n) => ({
210
+ text: n.text,
211
+ resourceId: n.resourceId,
212
+ contentDesc: n.contentDesc,
213
+ className: n.className,
214
+ hintText: n.hintText,
215
+ bounds: n.bounds,
216
+ clickable: n.clickable,
217
+ focusable: n.focusable,
218
+ enabled: n.enabled,
219
+ checked: n.checked,
220
+ checkable: n.checkable,
221
+ focused: n.focused,
222
+ selected: n.selected,
223
+ visibleToUser: n.visibleToUser,
224
+ }));
225
+ result.childrenOf = childrenOf;
226
+ result.roots = roots;
227
+ return result;
228
+ }
229
+ function buildAndroidA11y(xml) {
230
+ const tree = parseAndroidTree(xml);
231
+ const snapshot = [];
232
+ const meta = new Array(tree.length);
233
+ const computeMeta = (rawIdx) => {
234
+ const n = tree[rawIdx];
235
+ const role = androidRoleFor(n.className);
236
+ const screenReaderFocusable = n.focusable && (!!n.text || !!n.contentDesc);
237
+ const importantForAccessibility = screenReaderFocusable ? 'yes' : 'auto';
238
+ const stateParts = [];
239
+ if (n.checkable)
240
+ stateParts.push(n.checked ? 'checked' : 'not checked');
241
+ if (n.selected)
242
+ stateParts.push('selected');
243
+ if (!n.enabled)
244
+ stateParts.push('disabled');
245
+ const announcement = composeAndroidAnnouncement(n.text, n.contentDesc, role, stateParts, n.hintText);
246
+ // importantForAccessibility is inferred as 'yes' or 'auto' here (uiautomator
247
+ // dumps omit 'no' nodes). The 'no' case is kept as a documented future extension.
248
+ const wouldAnnounce = n.visibleToUser && (!!n.text || !!n.contentDesc || screenReaderFocusable);
249
+ return {
250
+ role,
251
+ screenReaderFocusable,
252
+ importantForAccessibility,
253
+ announcement,
254
+ stateParts,
255
+ wouldAnnounce,
256
+ hasAnnouncedDescendant: false,
257
+ };
258
+ };
259
+ // Post-order pass to fill hasAnnouncedDescendant.
260
+ const postOrder = (rawIdx) => {
261
+ meta[rawIdx] = computeMeta(rawIdx);
262
+ let anyChild = false;
263
+ for (const c of tree.childrenOf.get(rawIdx) ?? []) {
264
+ const childHasAnnounced = postOrder(c);
265
+ if (childHasAnnounced || meta[c].wouldAnnounce)
266
+ anyChild = true;
267
+ }
268
+ meta[rawIdx].hasAnnouncedDescendant = anyChild;
269
+ return anyChild;
270
+ };
271
+ for (const r of tree.roots)
272
+ postOrder(r);
273
+ // Phase 2: pre-order walk to emit snapshot entries + build enriched tree.
274
+ let order = 0;
275
+ const walk = (rawIdx, path) => {
276
+ const n = tree[rawIdx];
277
+ const m = meta[rawIdx];
278
+ const inOrder = m.wouldAnnounce && !m.hasAnnouncedDescendant;
279
+ let accessibilityOrder = null;
280
+ if (inOrder) {
281
+ accessibilityOrder = order++;
282
+ snapshot.push({
283
+ nodeId: path,
284
+ order: accessibilityOrder,
285
+ frame: {
286
+ x: n.bounds.x1,
287
+ y: n.bounds.y1,
288
+ w: n.bounds.x2 - n.bounds.x1,
289
+ h: n.bounds.y2 - n.bounds.y1,
290
+ },
291
+ label: n.contentDesc || n.text,
292
+ hint: n.hintText,
293
+ role: m.role,
294
+ traits: m.role ? [m.role] : [],
295
+ announcement: m.announcement,
296
+ value: '',
297
+ state: {
298
+ enabled: n.enabled,
299
+ selected: n.selected,
300
+ focused: n.focused,
301
+ checked: n.checkable ? n.checked : undefined,
302
+ },
303
+ });
304
+ }
305
+ const childIndices = tree.childrenOf.get(rawIdx) ?? [];
306
+ const kids = childIndices.map((i, ci) => walk(i, `${path}.${ci}`));
307
+ return {
308
+ nodeId: path,
309
+ accessibilityOrder,
310
+ class: n.className,
311
+ resourceId: n.resourceId,
312
+ text: n.text,
313
+ contentDescription: n.contentDesc,
314
+ hintText: n.hintText,
315
+ roleDescription: m.role,
316
+ role: m.role,
317
+ importantForAccessibility: m.importantForAccessibility,
318
+ screenReaderFocusable: m.screenReaderFocusable,
319
+ bounds: n.bounds,
320
+ state: {
321
+ enabled: n.enabled,
322
+ selected: n.selected,
323
+ focused: n.focused,
324
+ checked: n.checkable ? n.checked : undefined,
325
+ },
326
+ announcement: m.announcement,
327
+ children: kids.length ? kids : undefined,
328
+ };
329
+ };
330
+ const hierarchy = tree.roots.map((r, i) => walk(r, String(i)));
331
+ return { hierarchy, a11ySnapshot: snapshot };
332
+ }
333
+ const WEB_FOCUSABLE_ROLES = new Set([
334
+ 'button',
335
+ 'link',
336
+ 'textbox',
337
+ 'searchbox',
338
+ 'checkbox',
339
+ 'radio',
340
+ 'switch',
341
+ 'slider',
342
+ 'spinbutton',
343
+ 'combobox',
344
+ 'menuitem',
345
+ 'menuitemcheckbox',
346
+ 'menuitemradio',
347
+ 'tab',
348
+ 'option',
349
+ ]);
350
+ function isWebFocusable(node) {
351
+ return WEB_FOCUSABLE_ROLES.has(node.role);
352
+ }
353
+ /** Web announcement: `[accessibleName], [role], [state]` — screen readers read role after name. */
354
+ function composeWebAnnouncement(name, role, stateParts) {
355
+ const parts = [];
356
+ if (name)
357
+ parts.push(name);
358
+ if (role && role !== 'generic' && role !== 'none')
359
+ parts.push(role);
360
+ if (stateParts.length)
361
+ parts.push(stateParts.join(', '));
362
+ return parts.join(', ');
363
+ }
364
+ function buildWebA11y(hierarchy) {
365
+ const snapshot = [];
366
+ let order = 0;
367
+ function walk(nodes, basePath) {
368
+ return nodes.map((n, i) => {
369
+ const path = basePath === '' ? String(i) : `${basePath}.${i}`;
370
+ const focusable = isWebFocusable(n);
371
+ const stateParts = [];
372
+ if (n.checked)
373
+ stateParts.push('checked');
374
+ if (n.selected)
375
+ stateParts.push('selected');
376
+ if (!n.enabled)
377
+ stateParts.push('disabled');
378
+ const announcement = composeWebAnnouncement(n.name, n.role, stateParts);
379
+ const kids = n.children ? walk(n.children, path) : undefined;
380
+ let accessibilityOrder = null;
381
+ if (focusable && n.bounds && n.bounds.width > 0 && n.bounds.height > 0) {
382
+ accessibilityOrder = order++;
383
+ snapshot.push({
384
+ nodeId: path,
385
+ order: accessibilityOrder,
386
+ frame: { x: n.bounds.x, y: n.bounds.y, w: n.bounds.width, h: n.bounds.height },
387
+ label: n.name,
388
+ hint: '',
389
+ role: n.role,
390
+ traits: n.role ? [n.role] : [],
391
+ announcement,
392
+ value: '',
393
+ state: {
394
+ enabled: n.enabled,
395
+ selected: !!n.selected,
396
+ focused: n.focused,
397
+ checked: n.checked,
398
+ },
399
+ });
400
+ }
401
+ const enriched = {
402
+ ...n,
403
+ nodeId: path,
404
+ accessibilityOrder,
405
+ accessibleName: n.name,
406
+ ariaLabel: n.name, // Playwright's `name` is the computed accessible name; alias it.
407
+ ariaDescription: '',
408
+ focusable,
409
+ announcement,
410
+ children: kids,
411
+ };
412
+ return enriched;
413
+ });
414
+ }
415
+ return { hierarchy: walk(hierarchy.elements, ''), a11ySnapshot: snapshot };
416
+ }
@@ -35,6 +35,7 @@ exports.uninstallDriver = uninstallDriver;
35
35
  const child_process_1 = require("child_process");
36
36
  const crypto_1 = __importDefault(require("crypto"));
37
37
  const http_1 = __importDefault(require("http"));
38
+ const https_1 = __importDefault(require("https"));
38
39
  const net_1 = __importDefault(require("net"));
39
40
  const os_1 = __importDefault(require("os"));
40
41
  const fs_1 = __importDefault(require("fs"));
@@ -161,40 +162,195 @@ async function getDriverPort(platform, deviceId) {
161
162
  return port;
162
163
  });
163
164
  }
164
- // ── Bundled driver paths ───────────────────────────────────────────────────────
165
+ // ── Driver paths (bundled dev fallback + runtime download cache) ──────────────
165
166
  /**
166
- * Root of the bundled drivers directory (packages/cli/drivers/).
167
- *
168
167
  * Walk up from __dirname to find the package root (the directory containing
169
- * package.json). This handles both the normal build (dist/drivers/bootstrap.js)
170
- * and the test build (dist-tests/src/drivers/bootstrap.js) where __dirname has
171
- * an extra src/ level, making a fixed relative path incorrect.
168
+ * package.json). Handles both the normal build (dist/drivers/bootstrap.js)
169
+ * and the test build (dist-tests/src/drivers/bootstrap.js) where __dirname
170
+ * has an extra src/ level.
172
171
  */
173
- function findBundledDriversDir() {
172
+ function findPackageRoot() {
174
173
  let dir = __dirname;
175
174
  while (true) {
176
175
  if (fs_1.default.existsSync(path_1.default.join(dir, 'package.json'))) {
177
- return path_1.default.join(dir, 'drivers');
176
+ return dir;
178
177
  }
179
178
  const parent = path_1.default.dirname(dir);
180
179
  if (parent === dir)
181
180
  break;
182
181
  dir = parent;
183
182
  }
184
- // Fallback to original relative path
185
- return path_1.default.join(__dirname, '..', '..', 'drivers');
183
+ return path_1.default.join(__dirname, '..', '..');
184
+ }
185
+ const DRIVERS_CACHE_ROOT = path_1.default.join(os_1.default.homedir(), '.conductor', 'drivers');
186
+ const DRIVERS_DOWNLOAD_BASE = 'https://github.com/DouweBos/conductor/releases/download';
187
+ const DRIVERS_LOCK_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes (download can be slow)
188
+ const DRIVERS_LOCK_POLL_MS = 500;
189
+ let _driversDirPromise = null;
190
+ /**
191
+ * Resolve the directory containing the platform driver artifacts
192
+ * (`<dir>/{android,ios,tvos}/...`).
193
+ *
194
+ * Lookup order:
195
+ * 1. Legacy bundled drivers at `<pkg-root>/drivers/` — populated by
196
+ * `make build` for local development.
197
+ * 2. Runtime cache at `~/.conductor/drivers/<version>/` — downloaded
198
+ * on first use from the matching GitHub Release.
199
+ */
200
+ async function getDriversDir() {
201
+ if (_driversDirPromise)
202
+ return _driversDirPromise;
203
+ _driversDirPromise = (async () => {
204
+ const pkgRoot = findPackageRoot();
205
+ const legacyDir = path_1.default.join(pkgRoot, 'drivers');
206
+ if (fs_1.default.existsSync(legacyDir))
207
+ return legacyDir;
208
+ return await ensureDriversCache(pkgRoot);
209
+ })().catch((err) => {
210
+ _driversDirPromise = null;
211
+ throw err;
212
+ });
213
+ return _driversDirPromise;
214
+ }
215
+ async function ensureDriversCache(pkgRoot) {
216
+ const pkgJsonPath = path_1.default.join(pkgRoot, 'package.json');
217
+ const pkg = JSON.parse(fs_1.default.readFileSync(pkgJsonPath, 'utf-8'));
218
+ const version = pkg.version;
219
+ const cacheDir = path_1.default.join(DRIVERS_CACHE_ROOT, version);
220
+ const completeMarker = path_1.default.join(cacheDir, '.complete');
221
+ if (fs_1.default.existsSync(completeMarker))
222
+ return cacheDir;
223
+ fs_1.default.mkdirSync(DRIVERS_CACHE_ROOT, { recursive: true });
224
+ const lockFile = path_1.default.join(DRIVERS_CACHE_ROOT, `${version}.lock`);
225
+ await acquireDriversLock(lockFile);
226
+ try {
227
+ // Re-check after acquiring lock — another process may have finished.
228
+ if (fs_1.default.existsSync(completeMarker))
229
+ return cacheDir;
230
+ const tmpDir = path_1.default.join(DRIVERS_CACHE_ROOT, `.tmp-${version}-${process.pid}-${Date.now()}`);
231
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
232
+ fs_1.default.mkdirSync(tmpDir, { recursive: true });
233
+ const tarball = path_1.default.join(tmpDir, 'drivers.tar.gz');
234
+ const url = `${DRIVERS_DOWNLOAD_BASE}/v${version}/drivers.tar.gz`;
235
+ (0, verbose_js_1.log)(`Downloading conductor drivers v${version} from ${url}...`);
236
+ try {
237
+ await downloadToFile(url, tarball);
238
+ (0, child_process_1.execFileSync)('tar', ['-xzf', tarball, '-C', tmpDir], { stdio: 'ignore' });
239
+ fs_1.default.unlinkSync(tarball);
240
+ if (fs_1.default.existsSync(cacheDir)) {
241
+ fs_1.default.rmSync(cacheDir, { recursive: true, force: true });
242
+ }
243
+ fs_1.default.renameSync(tmpDir, cacheDir);
244
+ fs_1.default.writeFileSync(completeMarker, version);
245
+ (0, verbose_js_1.log)(`Conductor drivers v${version} ready at ${cacheDir}`);
246
+ pruneOldDriverCaches(version);
247
+ return cacheDir;
248
+ }
249
+ catch (err) {
250
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
251
+ throw new Error(`Failed to download conductor drivers v${version} from ${url}: ${err.message}`);
252
+ }
253
+ }
254
+ finally {
255
+ try {
256
+ fs_1.default.unlinkSync(lockFile);
257
+ }
258
+ catch {
259
+ /* ok */
260
+ }
261
+ }
262
+ }
263
+ /**
264
+ * Remove cached driver versions other than the current one. Old CLI builds
265
+ * would just re-download on demand, so there's no reason to keep them.
266
+ * Errors are swallowed — pruning is best-effort and must never block startup.
267
+ */
268
+ function pruneOldDriverCaches(currentVersion) {
269
+ try {
270
+ for (const entry of fs_1.default.readdirSync(DRIVERS_CACHE_ROOT, { withFileTypes: true })) {
271
+ if (!entry.isDirectory())
272
+ continue;
273
+ if (entry.name === currentVersion)
274
+ continue;
275
+ if (entry.name.startsWith('.tmp-'))
276
+ continue; // active concurrent extraction
277
+ const stale = path_1.default.join(DRIVERS_CACHE_ROOT, entry.name);
278
+ try {
279
+ fs_1.default.rmSync(stale, { recursive: true, force: true });
280
+ (0, verbose_js_1.log)(`Pruned stale driver cache ${stale}`);
281
+ }
282
+ catch {
283
+ /* ok — another process may be using it */
284
+ }
285
+ }
286
+ }
287
+ catch {
288
+ /* ok */
289
+ }
290
+ }
291
+ async function acquireDriversLock(lockFile) {
292
+ const deadline = Date.now() + DRIVERS_LOCK_TIMEOUT_MS;
293
+ while (Date.now() < deadline) {
294
+ try {
295
+ const fd = fs_1.default.openSync(lockFile, 'wx');
296
+ fs_1.default.closeSync(fd);
297
+ return;
298
+ }
299
+ catch {
300
+ await (0, utils_js_1.sleep)(DRIVERS_LOCK_POLL_MS);
301
+ }
302
+ }
303
+ throw new Error(`Could not acquire drivers cache lock (${lockFile})`);
304
+ }
305
+ function downloadToFile(url, dest, maxRedirects = 5) {
306
+ return new Promise((resolve, reject) => {
307
+ const fetch = (u, remaining) => {
308
+ const req = https_1.default.get(u, (res) => {
309
+ const status = res.statusCode ?? 0;
310
+ if ((status === 301 ||
311
+ status === 302 ||
312
+ status === 303 ||
313
+ status === 307 ||
314
+ status === 308) &&
315
+ res.headers.location) {
316
+ res.resume();
317
+ if (remaining <= 0) {
318
+ reject(new Error(`Too many redirects fetching ${url}`));
319
+ return;
320
+ }
321
+ const next = new URL(res.headers.location, u).toString();
322
+ fetch(next, remaining - 1);
323
+ return;
324
+ }
325
+ if (status !== 200) {
326
+ res.resume();
327
+ reject(new Error(`HTTP ${status} for ${u}`));
328
+ return;
329
+ }
330
+ const file = fs_1.default.createWriteStream(dest);
331
+ res.pipe(file);
332
+ file.on('finish', () => file.close((err) => (err ? reject(err) : resolve())));
333
+ file.on('error', (err) => {
334
+ fs_1.default.rmSync(dest, { force: true });
335
+ reject(err);
336
+ });
337
+ });
338
+ req.on('error', reject);
339
+ };
340
+ fetch(url, maxRedirects);
341
+ });
186
342
  }
187
- const BUNDLED_DRIVERS_DIR = findBundledDriversDir();
188
343
  /**
189
344
  * Install the Conductor Android driver APKs on the device.
190
345
  * Reads pre-built APKs directly from the bundled drivers directory.
191
346
  */
192
347
  async function installDriver(deviceId) {
193
348
  (0, verbose_js_1.log)(`installDriver: installing Android driver on ${deviceId}`);
194
- const appApk = path_1.default.join(BUNDLED_DRIVERS_DIR, 'android', 'conductor-app.apk');
195
- const serverApk = path_1.default.join(BUNDLED_DRIVERS_DIR, 'android', 'conductor-server.apk');
349
+ const driversDir = await getDriversDir();
350
+ const appApk = path_1.default.join(driversDir, 'android', 'conductor-app.apk');
351
+ const serverApk = path_1.default.join(driversDir, 'android', 'conductor-server.apk');
196
352
  if (!fs_1.default.existsSync(appApk) || !fs_1.default.existsSync(serverApk)) {
197
- throw new Error(`Conductor driver APKs not found at ${path_1.default.join(BUNDLED_DRIVERS_DIR, 'android')}.\n` +
353
+ throw new Error(`Conductor driver APKs not found at ${path_1.default.join(driversDir, 'android')}.\n` +
198
354
  `Run 'make package-cli' from the repo root to build and bundle the drivers.`);
199
355
  }
200
356
  await spawnAndWait('adb', ['-s', deviceId, 'install', '-r', '-t', '-g', appApk]);
@@ -214,13 +370,14 @@ const IOS_DRIVER_CACHE = path_1.default.join(os_1.default.homedir(), '.conductor
214
370
  * dir. Re-extracts only when the bundled xctestrun has changed (tracked by mtime).
215
371
  */
216
372
  async function setupIOSDriverCache() {
217
- const bundledXctestrun = path_1.default.join(BUNDLED_DRIVERS_DIR, 'ios', 'conductor-driver-ios-config.xctestrun');
218
- const bundledDriverZip = path_1.default.join(BUNDLED_DRIVERS_DIR, 'ios', 'conductor-driver-ios.zip');
219
- const bundledRunnerZip = path_1.default.join(BUNDLED_DRIVERS_DIR, 'ios', 'conductor-driver-iosUITests-Runner.zip');
373
+ const driversDir = await getDriversDir();
374
+ const bundledXctestrun = path_1.default.join(driversDir, 'ios', 'conductor-driver-ios-config.xctestrun');
375
+ const bundledDriverZip = path_1.default.join(driversDir, 'ios', 'conductor-driver-ios.zip');
376
+ const bundledRunnerZip = path_1.default.join(driversDir, 'ios', 'conductor-driver-iosUITests-Runner.zip');
220
377
  if (!fs_1.default.existsSync(bundledXctestrun) ||
221
378
  !fs_1.default.existsSync(bundledDriverZip) ||
222
379
  !fs_1.default.existsSync(bundledRunnerZip)) {
223
- throw new Error(`Conductor iOS driver files not found at ${path_1.default.join(BUNDLED_DRIVERS_DIR, 'ios')}.\n` +
380
+ throw new Error(`Conductor iOS driver files not found at ${path_1.default.join(driversDir, 'ios')}.\n` +
224
381
  `Run 'make package-cli' from the repo root to build and bundle the drivers.`);
225
382
  }
226
383
  const versionFile = path_1.default.join(IOS_DRIVER_CACHE, '.version');
@@ -335,13 +492,14 @@ const TVOS_DRIVER_CACHE = path_1.default.join(os_1.default.homedir(), '.conducto
335
492
  * dir. Re-extracts only when the bundled xctestrun has changed (tracked by mtime).
336
493
  */
337
494
  async function setupTvOSDriverCache() {
338
- const bundledXctestrun = path_1.default.join(BUNDLED_DRIVERS_DIR, 'tvos', 'conductor-driver-tvos-config.xctestrun');
339
- const bundledDriverZip = path_1.default.join(BUNDLED_DRIVERS_DIR, 'tvos', 'conductor-driver-tvos.zip');
340
- const bundledRunnerZip = path_1.default.join(BUNDLED_DRIVERS_DIR, 'tvos', 'conductor-driver-tvosUITests-Runner.zip');
495
+ const driversDir = await getDriversDir();
496
+ const bundledXctestrun = path_1.default.join(driversDir, 'tvos', 'conductor-driver-tvos-config.xctestrun');
497
+ const bundledDriverZip = path_1.default.join(driversDir, 'tvos', 'conductor-driver-tvos.zip');
498
+ const bundledRunnerZip = path_1.default.join(driversDir, 'tvos', 'conductor-driver-tvosUITests-Runner.zip');
341
499
  if (!fs_1.default.existsSync(bundledXctestrun) ||
342
500
  !fs_1.default.existsSync(bundledDriverZip) ||
343
501
  !fs_1.default.existsSync(bundledRunnerZip)) {
344
- throw new Error(`Conductor tvOS driver files not found at ${path_1.default.join(BUNDLED_DRIVERS_DIR, 'tvos')}.\n` +
502
+ throw new Error(`Conductor tvOS driver files not found at ${path_1.default.join(driversDir, 'tvos')}.\n` +
345
503
  `Run 'make package-cli' from the repo root to build and bundle the drivers.`);
346
504
  }
347
505
  const versionFile = path_1.default.join(TVOS_DRIVER_CACHE, '.version');
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ const scroll_js_1 = require("./commands/scroll.js");
18
18
  const swipe_js_1 = require("./commands/swipe.js");
19
19
  const assert_visible_js_1 = require("./commands/assert-visible.js");
20
20
  const screenshot_js_1 = require("./commands/screenshot.js");
21
+ const capture_ui_js_1 = require("./commands/capture-ui.js");
21
22
  const inspect_js_1 = require("./commands/inspect.js");
22
23
  const focused_js_1 = require("./commands/focused.js");
23
24
  const run_flow_js_1 = require("./commands/run-flow.js");
@@ -79,6 +80,7 @@ const COMMAND_HELP = {
79
80
  'set-location': set_location_js_1.HELP,
80
81
  'set-orientation': set_orientation_js_1.HELP,
81
82
  'take-screenshot': screenshot_js_1.HELP,
83
+ 'capture-ui': capture_ui_js_1.HELP,
82
84
  inspect: inspect_js_1.HELP,
83
85
  focused: focused_js_1.HELP,
84
86
  'run-flow': run_flow_js_1.HELP,
@@ -454,6 +456,11 @@ async function main() {
454
456
  exitCode = await (0, screenshot_js_1.screenshot)(outPath, opts, sessionName);
455
457
  break;
456
458
  }
459
+ case 'capture-ui': {
460
+ const outPath = argv['output'];
461
+ exitCode = await (0, capture_ui_js_1.captureUI)(outPath, opts, sessionName);
462
+ break;
463
+ }
457
464
  case 'inspect':
458
465
  exitCode = await (0, inspect_js_1.inspect)(opts, sessionName, { dump: argv['dump'] });
459
466
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@houwert/conductor",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "CLI tool for mobile app interactions — optimized for AI agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -17,7 +17,6 @@
17
17
  "main": "./dist/index.js",
18
18
  "files": [
19
19
  "dist/",
20
- "drivers/",
21
20
  "skills/",
22
21
  "proto/",
23
22
  ".claude-plugin/"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: conductor
3
- version: 0.9.0
3
+ version: 0.11.0
4
4
  description: "Token-efficient CLI for mobile UI testing (iOS simulator + Android emulator), designed for AI agents"
5
5
  metadata.openclaw:
6
6
  category: service
@@ -410,8 +410,54 @@ The hierarchy shows each element's type, text, and accessibility ID (resourceId
410
410
 
411
411
  ```bash
412
412
  conductor inspect
413
+ conductor inspect --dump # full hierarchy as JSON with a11y fields
413
414
  ```
414
415
 
416
+ `--dump` emits a11y-enriched JSON on all platforms. Each node carries
417
+ `accessibilityOrder` (0-based screen-reader nav index, or `null`), `traits` / `role`,
418
+ `nodeId` (stable per-capture path), and platform-specific fields:
419
+ `isAccessibilityElement` + `accessibilityLabel/Hint/Value` on iOS,
420
+ `contentDescription` + `screenReaderFocusable` + `importantForAccessibility` on Android,
421
+ `accessibleName` + `focusable` on web. Existing fields (`label`, `frame`, `identifier`,
422
+ `text`, `resource-id`, `bounds`, ...) are preserved.
423
+
424
+ ---
425
+
426
+ ### `capture-ui`
427
+
428
+ Bundle everything Argus needs for a single UI inspection: screenshot (PNG base64) +
429
+ full UI hierarchy (with a11y fields) + flat accessibility-order snapshot in one JSON
430
+ document. Intended primarily for programmatic consumers (Argus UI panel); humans
431
+ should prefer `inspect` + `take-screenshot`.
432
+
433
+ ```bash
434
+ conductor capture-ui # prints bundle as JSON to stdout
435
+ conductor capture-ui --output /tmp/capture.json # writes bundle to file
436
+ ```
437
+
438
+ Output shape:
439
+
440
+ ```json
441
+ {
442
+ "version": 1,
443
+ "capturedAt": "2026-04-22T12:34:56.000Z",
444
+ "device": { "platform": "ios", "deviceId": "...", "width": 390, "height": 844 },
445
+ "screenshot": { "kind": "composite", "encoding": "png", "data": "<base64>" },
446
+ "hierarchy": { /* platform-native hierarchy with a11y fields on each node */ },
447
+ "a11ySnapshot": [
448
+ { "nodeId": "0.2.1", "order": 0, "frame": {"x":0,"y":0,"w":80,"h":44},
449
+ "label": "Sign in", "hint": "", "role": "button", "traits": ["button"],
450
+ "announcement": "Sign in, button", "value": "",
451
+ "state": {"enabled": true, "selected": false, "focused": false} }
452
+ ],
453
+ "capabilities": { "perViewPixels": false, "depthData": false }
454
+ }
455
+ ```
456
+
457
+ `nodeId` is a stable per-capture path (dot-joined child indices) so Argus can
458
+ correlate flat snapshot entries back to hierarchy nodes. `capabilities.perViewPixels`
459
+ is `false` in v1; per-view pixel rendering is a future phase.
460
+
415
461
  ---
416
462
 
417
463
  ### `logs`
@@ -3,6 +3,6 @@ skills:
3
3
  path: conductor/SKILL.md
4
4
  description: "Token-efficient CLI for mobile UI testing, designed for AI agents"
5
5
  category: service
6
- version: 0.9.0
6
+ version: 0.11.0
7
7
  requires:
8
8
  bins: [conductor]
Binary file
@@ -1,126 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
- <plist version="1.0">
4
- <dict>
5
- <key>__xctestrun_metadata__</key>
6
- <dict>
7
- <key>ContainerInfo</key>
8
- <dict>
9
- <key>ContainerName</key>
10
- <string>conductor-driver-ios</string>
11
- <key>SchemeName</key>
12
- <string>conductor-driver-ios</string>
13
- </dict>
14
- <key>FormatVersion</key>
15
- <integer>1</integer>
16
- </dict>
17
- <key>conductor-driver-iosUITests</key>
18
- <dict>
19
- <key>BlueprintName</key>
20
- <string>conductor-driver-iosUITests</string>
21
- <key>BlueprintProviderName</key>
22
- <string>conductor-driver-ios</string>
23
- <key>BlueprintProviderRelativePath</key>
24
- <string>conductor-driver-ios.xcodeproj</string>
25
- <key>BundleIdentifiersForCrashReportEmphasis</key>
26
- <array>
27
- <string>dev.houwert.ConductorDriverLib</string>
28
- <string>dev.houwert.conductor-driver-ios</string>
29
- <string>dev.houwert.conductor-driver-iosUITests</string>
30
- </array>
31
- <key>CommandLineArguments</key>
32
- <array/>
33
- <key>DefaultTestExecutionTimeAllowance</key>
34
- <integer>600</integer>
35
- <key>DependentProductPaths</key>
36
- <array>
37
- <string>__TESTROOT__/Debug-iphonesimulator/ConductorDriverLib.framework</string>
38
- <string>__TESTROOT__/Debug-iphonesimulator/conductor-driver-ios.app</string>
39
- <string>__TESTROOT__/Debug-iphonesimulator/conductor-driver-iosUITests-Runner.app</string>
40
- <string>__TESTROOT__/Debug-iphonesimulator/conductor-driver-iosUITests-Runner.app/PlugIns/conductor-driver-iosUITests.xctest</string>
41
- </array>
42
- <key>DiagnosticCollectionPolicy</key>
43
- <integer>1</integer>
44
- <key>EnvironmentVariables</key>
45
- <dict>
46
- <key>APP_DISTRIBUTOR_ID_OVERRIDE</key>
47
- <string>com.apple.AppStore</string>
48
- <key>OS_ACTIVITY_DT_MODE</key>
49
- <string>YES</string>
50
- <key>SQLITE_ENABLE_THREAD_ASSERTIONS</key>
51
- <string>1</string>
52
- <key>TERM</key>
53
- <string>dumb</string>
54
- </dict>
55
- <key>IsUITestBundle</key>
56
- <true/>
57
- <key>IsXCTRunnerHostedTestBundle</key>
58
- <true/>
59
- <key>PreferredScreenCaptureFormat</key>
60
- <string>screenRecording</string>
61
- <key>ProductModuleName</key>
62
- <string>conductor_driver_iosUITests</string>
63
- <key>RunOrder</key>
64
- <integer>0</integer>
65
- <key>SkipTestIdentifiers</key>
66
- <array>
67
- <string>ViewHierarchyHandlerTests</string>
68
- <string>ViewHierarchyHandlerTests/testViewHierarchyHandlerReturnsNonEmptyHierarchy()</string>
69
- </array>
70
- <key>SystemAttachmentLifetime</key>
71
- <string>deleteOnSuccess</string>
72
- <key>TestBundlePath</key>
73
- <string>__TESTHOST__/PlugIns/conductor-driver-iosUITests.xctest</string>
74
- <key>TestHostBundleIdentifier</key>
75
- <string>dev.houwert.conductor-driver-iosUITests.xctrunner</string>
76
- <key>TestHostPath</key>
77
- <string>__TESTROOT__/Debug-iphonesimulator/conductor-driver-iosUITests-Runner.app</string>
78
- <key>TestLanguage</key>
79
- <string></string>
80
- <key>TestRegion</key>
81
- <string></string>
82
- <key>TestTimeoutsEnabled</key>
83
- <false/>
84
- <key>TestingEnvironmentVariables</key>
85
- <dict>
86
- <key>DYLD_FRAMEWORK_PATH</key>
87
- <string>__TESTROOT__/Debug-iphonesimulator:__TESTROOT__/Debug-iphonesimulator/PackageFrameworks:__PLATFORMS__/iPhoneSimulator.platform/Developer/Library/Frameworks</string>
88
- <key>DYLD_LIBRARY_PATH</key>
89
- <string>__TESTROOT__/Debug-iphonesimulator:__PLATFORMS__/iPhoneSimulator.platform/Developer/usr/lib</string>
90
- <key>XCODE_SCHEME_NAME</key>
91
- <string>conductor-driver-ios</string>
92
- <key>__XCODE_BUILT_PRODUCTS_DIR_PATHS</key>
93
- <string>__TESTROOT__/Debug-iphonesimulator</string>
94
- <key>__XPC_DYLD_FRAMEWORK_PATH</key>
95
- <string>__TESTROOT__/Debug-iphonesimulator</string>
96
- <key>__XPC_DYLD_LIBRARY_PATH</key>
97
- <string>__TESTROOT__/Debug-iphonesimulator</string>
98
- </dict>
99
- <key>ToolchainsSettingValue</key>
100
- <array/>
101
- <key>UITargetAppCommandLineArguments</key>
102
- <array/>
103
- <key>UITargetAppEnvironmentVariables</key>
104
- <dict>
105
- <key>APP_DISTRIBUTOR_ID_OVERRIDE</key>
106
- <string>com.apple.AppStore</string>
107
- <key>DYLD_FRAMEWORK_PATH</key>
108
- <string>__TESTROOT__/Debug-iphonesimulator:__TESTROOT__/Debug-iphonesimulator/PackageFrameworks</string>
109
- <key>DYLD_LIBRARY_PATH</key>
110
- <string>__TESTROOT__/Debug-iphonesimulator</string>
111
- <key>XCODE_SCHEME_NAME</key>
112
- <string>conductor-driver-ios</string>
113
- <key>__XCODE_BUILT_PRODUCTS_DIR_PATHS</key>
114
- <string>__TESTROOT__/Debug-iphonesimulator</string>
115
- <key>__XPC_DYLD_FRAMEWORK_PATH</key>
116
- <string>__TESTROOT__/Debug-iphonesimulator</string>
117
- <key>__XPC_DYLD_LIBRARY_PATH</key>
118
- <string>__TESTROOT__/Debug-iphonesimulator</string>
119
- </dict>
120
- <key>UITargetAppPath</key>
121
- <string>__TESTROOT__/Debug-iphonesimulator/conductor-driver-ios.app</string>
122
- <key>UserAttachmentLifetime</key>
123
- <string>deleteOnSuccess</string>
124
- </dict>
125
- </dict>
126
- </plist>
@@ -1,121 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
- <plist version="1.0">
4
- <dict>
5
- <key>__xctestrun_metadata__</key>
6
- <dict>
7
- <key>ContainerInfo</key>
8
- <dict>
9
- <key>ContainerName</key>
10
- <string>conductor-driver-ios</string>
11
- <key>SchemeName</key>
12
- <string>conductor-driver-tvos</string>
13
- </dict>
14
- <key>FormatVersion</key>
15
- <integer>1</integer>
16
- </dict>
17
- <key>conductor-driver-tvosUITests</key>
18
- <dict>
19
- <key>BlueprintName</key>
20
- <string>conductor-driver-tvosUITests</string>
21
- <key>BlueprintProviderName</key>
22
- <string>conductor-driver-ios</string>
23
- <key>BlueprintProviderRelativePath</key>
24
- <string>conductor-driver-ios.xcodeproj</string>
25
- <key>BundleIdentifiersForCrashReportEmphasis</key>
26
- <array>
27
- <string>dev.houwert.ConductorDriverLib</string>
28
- <string>dev.houwert.conductor-driver-tvos</string>
29
- <string>dev.houwert.conductor-driver-tvosUITests</string>
30
- </array>
31
- <key>CommandLineArguments</key>
32
- <array/>
33
- <key>DefaultTestExecutionTimeAllowance</key>
34
- <integer>600</integer>
35
- <key>DependentProductPaths</key>
36
- <array>
37
- <string>__TESTROOT__/Debug-appletvsimulator/ConductorDriverLib.framework</string>
38
- <string>__TESTROOT__/Debug-appletvsimulator/conductor-driver-tvos.app</string>
39
- <string>__TESTROOT__/Debug-appletvsimulator/conductor-driver-tvosUITests-Runner.app</string>
40
- <string>__TESTROOT__/Debug-appletvsimulator/conductor-driver-tvosUITests-Runner.app/PlugIns/conductor-driver-tvosUITests.xctest</string>
41
- </array>
42
- <key>DiagnosticCollectionPolicy</key>
43
- <integer>1</integer>
44
- <key>EnvironmentVariables</key>
45
- <dict>
46
- <key>APP_DISTRIBUTOR_ID_OVERRIDE</key>
47
- <string>com.apple.AppStore</string>
48
- <key>OS_ACTIVITY_DT_MODE</key>
49
- <string>YES</string>
50
- <key>SQLITE_ENABLE_THREAD_ASSERTIONS</key>
51
- <string>1</string>
52
- <key>TERM</key>
53
- <string>dumb</string>
54
- </dict>
55
- <key>IsUITestBundle</key>
56
- <true/>
57
- <key>IsXCTRunnerHostedTestBundle</key>
58
- <true/>
59
- <key>PreferredScreenCaptureFormat</key>
60
- <string>screenRecording</string>
61
- <key>ProductModuleName</key>
62
- <string>conductor_driver_tvosUITests</string>
63
- <key>RunOrder</key>
64
- <integer>0</integer>
65
- <key>SystemAttachmentLifetime</key>
66
- <string>deleteOnSuccess</string>
67
- <key>TestBundlePath</key>
68
- <string>__TESTHOST__/PlugIns/conductor-driver-tvosUITests.xctest</string>
69
- <key>TestHostBundleIdentifier</key>
70
- <string>dev.houwert.conductor-driver-tvosUITests.xctrunner</string>
71
- <key>TestHostPath</key>
72
- <string>__TESTROOT__/Debug-appletvsimulator/conductor-driver-tvosUITests-Runner.app</string>
73
- <key>TestLanguage</key>
74
- <string></string>
75
- <key>TestRegion</key>
76
- <string></string>
77
- <key>TestTimeoutsEnabled</key>
78
- <false/>
79
- <key>TestingEnvironmentVariables</key>
80
- <dict>
81
- <key>DYLD_FRAMEWORK_PATH</key>
82
- <string>__TESTROOT__/Debug-appletvsimulator:__TESTROOT__/Debug-appletvsimulator/PackageFrameworks:__PLATFORMS__/AppleTVSimulator.platform/Developer/Library/Frameworks</string>
83
- <key>DYLD_LIBRARY_PATH</key>
84
- <string>__TESTROOT__/Debug-appletvsimulator:__PLATFORMS__/AppleTVSimulator.platform/Developer/usr/lib</string>
85
- <key>XCODE_SCHEME_NAME</key>
86
- <string>conductor-driver-tvos</string>
87
- <key>__XCODE_BUILT_PRODUCTS_DIR_PATHS</key>
88
- <string>__TESTROOT__/Debug-appletvsimulator</string>
89
- <key>__XPC_DYLD_FRAMEWORK_PATH</key>
90
- <string>__TESTROOT__/Debug-appletvsimulator</string>
91
- <key>__XPC_DYLD_LIBRARY_PATH</key>
92
- <string>__TESTROOT__/Debug-appletvsimulator</string>
93
- </dict>
94
- <key>ToolchainsSettingValue</key>
95
- <array/>
96
- <key>UITargetAppCommandLineArguments</key>
97
- <array/>
98
- <key>UITargetAppEnvironmentVariables</key>
99
- <dict>
100
- <key>APP_DISTRIBUTOR_ID_OVERRIDE</key>
101
- <string>com.apple.AppStore</string>
102
- <key>DYLD_FRAMEWORK_PATH</key>
103
- <string>__TESTROOT__/Debug-appletvsimulator:__TESTROOT__/Debug-appletvsimulator/PackageFrameworks</string>
104
- <key>DYLD_LIBRARY_PATH</key>
105
- <string>__TESTROOT__/Debug-appletvsimulator</string>
106
- <key>XCODE_SCHEME_NAME</key>
107
- <string>conductor-driver-tvos</string>
108
- <key>__XCODE_BUILT_PRODUCTS_DIR_PATHS</key>
109
- <string>__TESTROOT__/Debug-appletvsimulator</string>
110
- <key>__XPC_DYLD_FRAMEWORK_PATH</key>
111
- <string>__TESTROOT__/Debug-appletvsimulator</string>
112
- <key>__XPC_DYLD_LIBRARY_PATH</key>
113
- <string>__TESTROOT__/Debug-appletvsimulator</string>
114
- </dict>
115
- <key>UITargetAppPath</key>
116
- <string>__TESTROOT__/Debug-appletvsimulator/conductor-driver-tvos.app</string>
117
- <key>UserAttachmentLifetime</key>
118
- <string>deleteOnSuccess</string>
119
- </dict>
120
- </dict>
121
- </plist>