@kritchoff/agent-browser 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +903 -0
  3. package/README.sdk.md +77 -0
  4. package/bin/agent-browser-linux-x64 +0 -0
  5. package/bin/agent-browser.js +109 -0
  6. package/dist/actions.d.ts +17 -0
  7. package/dist/actions.d.ts.map +1 -0
  8. package/dist/actions.js +1427 -0
  9. package/dist/actions.js.map +1 -0
  10. package/dist/browser.d.ts +474 -0
  11. package/dist/browser.d.ts.map +1 -0
  12. package/dist/browser.js +1566 -0
  13. package/dist/browser.js.map +1 -0
  14. package/dist/cdp-client.d.ts +103 -0
  15. package/dist/cdp-client.d.ts.map +1 -0
  16. package/dist/cdp-client.js +223 -0
  17. package/dist/cdp-client.js.map +1 -0
  18. package/dist/daemon.d.ts +60 -0
  19. package/dist/daemon.d.ts.map +1 -0
  20. package/dist/daemon.js +401 -0
  21. package/dist/daemon.js.map +1 -0
  22. package/dist/dualmode-config.d.ts +37 -0
  23. package/dist/dualmode-config.d.ts.map +1 -0
  24. package/dist/dualmode-config.js +44 -0
  25. package/dist/dualmode-config.js.map +1 -0
  26. package/dist/dualmode-fetcher.d.ts +60 -0
  27. package/dist/dualmode-fetcher.d.ts.map +1 -0
  28. package/dist/dualmode-fetcher.js +449 -0
  29. package/dist/dualmode-fetcher.js.map +1 -0
  30. package/dist/dualmode-types.d.ts +183 -0
  31. package/dist/dualmode-types.d.ts.map +1 -0
  32. package/dist/dualmode-types.js +8 -0
  33. package/dist/dualmode-types.js.map +1 -0
  34. package/dist/ios-actions.d.ts +11 -0
  35. package/dist/ios-actions.d.ts.map +1 -0
  36. package/dist/ios-actions.js +228 -0
  37. package/dist/ios-actions.js.map +1 -0
  38. package/dist/ios-manager.d.ts +266 -0
  39. package/dist/ios-manager.d.ts.map +1 -0
  40. package/dist/ios-manager.js +1073 -0
  41. package/dist/ios-manager.js.map +1 -0
  42. package/dist/protocol.d.ts +26 -0
  43. package/dist/protocol.d.ts.map +1 -0
  44. package/dist/protocol.js +832 -0
  45. package/dist/protocol.js.map +1 -0
  46. package/dist/snapshot.d.ts +83 -0
  47. package/dist/snapshot.d.ts.map +1 -0
  48. package/dist/snapshot.js +653 -0
  49. package/dist/snapshot.js.map +1 -0
  50. package/dist/stream-server.d.ts +117 -0
  51. package/dist/stream-server.d.ts.map +1 -0
  52. package/dist/stream-server.js +305 -0
  53. package/dist/stream-server.js.map +1 -0
  54. package/dist/types.d.ts +742 -0
  55. package/dist/types.d.ts.map +1 -0
  56. package/dist/types.js +2 -0
  57. package/dist/types.js.map +1 -0
  58. package/docker-compose.sdk.yml +45 -0
  59. package/package.json +85 -0
  60. package/scripts/benchmark.sh +80 -0
  61. package/scripts/build-all-platforms.sh +68 -0
  62. package/scripts/check-version-sync.js +39 -0
  63. package/scripts/copy-native.js +36 -0
  64. package/scripts/fast_reset.sh +108 -0
  65. package/scripts/postinstall.js +235 -0
  66. package/scripts/publish_images.sh +55 -0
  67. package/scripts/snapshot_manager.sh +293 -0
  68. package/scripts/start-android-agent.sh +49 -0
  69. package/scripts/sync-version.js +69 -0
  70. package/scripts/vaccine-run +26 -0
  71. package/sdk.sh +153 -0
  72. package/skills/agent-browser/SKILL.md +217 -0
  73. package/skills/agent-browser/references/authentication.md +202 -0
  74. package/skills/agent-browser/references/commands.md +259 -0
  75. package/skills/agent-browser/references/proxy-support.md +188 -0
  76. package/skills/agent-browser/references/session-management.md +193 -0
  77. package/skills/agent-browser/references/snapshot-refs.md +194 -0
  78. package/skills/agent-browser/references/video-recording.md +173 -0
  79. package/skills/agent-browser/templates/authenticated-session.sh +97 -0
  80. package/skills/agent-browser/templates/capture-workflow.sh +69 -0
  81. package/skills/agent-browser/templates/form-automation.sh +62 -0
  82. package/skills/skill-creator/LICENSE.txt +202 -0
  83. package/skills/skill-creator/SKILL.md +356 -0
  84. package/skills/skill-creator/references/output-patterns.md +82 -0
  85. package/skills/skill-creator/references/workflows.md +28 -0
  86. package/skills/skill-creator/scripts/init_skill.py +303 -0
  87. package/skills/skill-creator/scripts/package_skill.py +113 -0
  88. package/skills/skill-creator/scripts/quick_validate.py +95 -0
@@ -0,0 +1,1073 @@
1
+ /**
2
+ * iOS Simulator Manager - Manages iOS Simulator and Safari automation via Appium.
3
+ *
4
+ * This provides 1:1 command parity with BrowserManager for iOS Safari.
5
+ */
6
+ import { Simctl } from 'node-simctl';
7
+ import { remote } from 'webdriverio';
8
+ import { spawn } from 'node:child_process';
9
+ import { existsSync } from 'node:fs';
10
+ import path from 'node:path';
11
+ /**
12
+ * Manages iOS Simulator and Safari automation via Appium
13
+ */
14
+ export class IOSManager {
15
+ simctl;
16
+ browser = null;
17
+ appiumProcess = null;
18
+ deviceUdid = null;
19
+ deviceName = null;
20
+ consoleMessages = [];
21
+ refMap = {};
22
+ lastSnapshot = '';
23
+ refCounter = 0;
24
+ // Default Appium port
25
+ static APPIUM_PORT = 4723;
26
+ static APPIUM_HOST = '127.0.0.1';
27
+ constructor() {
28
+ this.simctl = new Simctl();
29
+ }
30
+ /**
31
+ * Check if browser is launched
32
+ */
33
+ isLaunched() {
34
+ return this.browser !== null;
35
+ }
36
+ /**
37
+ * List connected real iOS devices
38
+ */
39
+ async listRealDevices() {
40
+ const devices = [];
41
+ try {
42
+ // Use xcrun xctrace to list connected devices
43
+ const { execSync } = await import('node:child_process');
44
+ const output = execSync('xcrun xctrace list devices 2>/dev/null || true', {
45
+ encoding: 'utf-8',
46
+ timeout: 10000,
47
+ });
48
+ // Parse output - format is:
49
+ // == Devices ==
50
+ // Device Name (OS Version) (UDID)
51
+ // Real devices show version as just "26.2", simulators as "iOS 18.0"
52
+ const lines = output.split('\n');
53
+ let inDevicesSection = false;
54
+ for (const line of lines) {
55
+ if (line.includes('== Devices ==')) {
56
+ inDevicesSection = true;
57
+ continue;
58
+ }
59
+ // Stop at Simulators or Devices Offline section
60
+ if (line.includes('== Simulators ==') || line.includes('== Devices Offline ==')) {
61
+ break;
62
+ }
63
+ if (inDevicesSection && line.trim()) {
64
+ // Match pattern: "Device Name (version) (UDID)"
65
+ const match = line.match(/^(.+?)\s+\(([^)]+)\)\s+\(([A-F0-9-]+)\)$/i);
66
+ if (match) {
67
+ const [, name, version, udid] = match;
68
+ const nameLower = name.toLowerCase();
69
+ // Include iOS devices: either name contains iPhone/iPad, or version looks like iOS
70
+ // (a simple version number like "26.2" or "18.6") and isn't a Mac
71
+ const isIOS = nameLower.includes('iphone') ||
72
+ nameLower.includes('ipad') ||
73
+ version.includes('iOS') ||
74
+ version.includes('iPadOS');
75
+ const isMac = nameLower.includes('mac') ||
76
+ nameLower.includes('macbook') ||
77
+ nameLower.includes('imac');
78
+ if (isIOS || (!isMac && /^\d+\.\d+(\.\d+)?$/.test(version))) {
79
+ devices.push({
80
+ name: name.trim(),
81
+ udid: udid,
82
+ state: 'Connected',
83
+ runtime: `iOS ${version}`,
84
+ isAvailable: true,
85
+ isRealDevice: true,
86
+ });
87
+ }
88
+ }
89
+ }
90
+ }
91
+ }
92
+ catch {
93
+ // Ignore errors - real device listing is optional
94
+ }
95
+ return devices;
96
+ }
97
+ /**
98
+ * List available iOS simulators
99
+ */
100
+ async listDevices() {
101
+ const devices = [];
102
+ try {
103
+ const rawDevices = await this.simctl.getDevices();
104
+ for (const [runtime, deviceList] of Object.entries(rawDevices)) {
105
+ if (!Array.isArray(deviceList))
106
+ continue;
107
+ for (const device of deviceList) {
108
+ // Only include iPhone and iPad simulators
109
+ if (device.name && (device.name.includes('iPhone') || device.name.includes('iPad'))) {
110
+ devices.push({
111
+ name: device.name,
112
+ udid: device.udid,
113
+ state: device.state,
114
+ runtime: runtime,
115
+ isAvailable: device.isAvailable ?? true,
116
+ isRealDevice: false,
117
+ });
118
+ }
119
+ }
120
+ }
121
+ }
122
+ catch (error) {
123
+ throw new Error(`Failed to list iOS simulators. Is Xcode installed? Error: ${error instanceof Error ? error.message : String(error)}`);
124
+ }
125
+ return devices;
126
+ }
127
+ /**
128
+ * List all devices (simulators + real devices)
129
+ */
130
+ async listAllDevices() {
131
+ const [simulators, realDevices] = await Promise.all([
132
+ this.listDevices(),
133
+ this.listRealDevices(),
134
+ ]);
135
+ // Real devices first, then simulators
136
+ return [...realDevices, ...simulators];
137
+ }
138
+ /**
139
+ * Find the best default device (most recent iPhone)
140
+ */
141
+ async findDefaultDevice() {
142
+ const devices = await this.listDevices();
143
+ // Filter to available iPhones, prefer Pro models, then by name (which typically indicates recency)
144
+ const iphones = devices
145
+ .filter((d) => d.isAvailable && d.name.includes('iPhone'))
146
+ .sort((a, b) => {
147
+ // Prefer Pro models
148
+ const aIsPro = a.name.includes('Pro') ? 1 : 0;
149
+ const bIsPro = b.name.includes('Pro') ? 1 : 0;
150
+ if (aIsPro !== bIsPro)
151
+ return bIsPro - aIsPro;
152
+ // Then sort by name descending (iPhone 15 > iPhone 14)
153
+ return b.name.localeCompare(a.name);
154
+ });
155
+ return iphones[0] ?? null;
156
+ }
157
+ /**
158
+ * Find device by name or UDID (searches both simulators and real devices)
159
+ */
160
+ async findDevice(nameOrUdid) {
161
+ const devices = await this.listAllDevices();
162
+ // Try exact UDID match first
163
+ const byUdid = devices.find((d) => d.udid === nameOrUdid);
164
+ if (byUdid)
165
+ return byUdid;
166
+ // Try exact name match
167
+ const byExactName = devices.find((d) => d.name === nameOrUdid);
168
+ if (byExactName)
169
+ return byExactName;
170
+ // Try partial name match
171
+ const byPartialName = devices.find((d) => d.name.toLowerCase().includes(nameOrUdid.toLowerCase()));
172
+ return byPartialName ?? null;
173
+ }
174
+ /**
175
+ * Check if Appium is installed
176
+ */
177
+ async checkAppiumInstalled() {
178
+ return new Promise((resolve) => {
179
+ const proc = spawn('appium', ['--version'], { shell: true });
180
+ proc.on('close', (code) => resolve(code === 0));
181
+ proc.on('error', () => resolve(false));
182
+ });
183
+ }
184
+ /**
185
+ * Check if Appium server is already running
186
+ */
187
+ async isAppiumRunning() {
188
+ try {
189
+ const response = await fetch(`http://${IOSManager.APPIUM_HOST}:${IOSManager.APPIUM_PORT}/status`);
190
+ return response.ok;
191
+ }
192
+ catch {
193
+ return false;
194
+ }
195
+ }
196
+ /**
197
+ * Start Appium server if not already running
198
+ */
199
+ async startAppiumServer() {
200
+ if (await this.isAppiumRunning()) {
201
+ return; // Already running
202
+ }
203
+ if (!(await this.checkAppiumInstalled())) {
204
+ throw new Error('Appium not installed. Run: npm install -g appium && appium driver install xcuitest');
205
+ }
206
+ return new Promise((resolve, reject) => {
207
+ this.appiumProcess = spawn('appium', ['--port', String(IOSManager.APPIUM_PORT), '--relaxed-security'], {
208
+ shell: true,
209
+ stdio: ['ignore', 'pipe', 'pipe'],
210
+ });
211
+ let started = false;
212
+ const timeout = setTimeout(() => {
213
+ if (!started) {
214
+ reject(new Error('Appium server failed to start within 30 seconds'));
215
+ }
216
+ }, 30000);
217
+ this.appiumProcess.stdout?.on('data', (data) => {
218
+ const output = data.toString();
219
+ if (output.includes('Appium REST http interface listener started')) {
220
+ started = true;
221
+ clearTimeout(timeout);
222
+ resolve();
223
+ }
224
+ });
225
+ this.appiumProcess.stderr?.on('data', (data) => {
226
+ const output = data.toString();
227
+ // Appium logs to stderr for info messages too
228
+ if (output.includes('Appium REST http interface listener started')) {
229
+ started = true;
230
+ clearTimeout(timeout);
231
+ resolve();
232
+ }
233
+ });
234
+ this.appiumProcess.on('error', (err) => {
235
+ clearTimeout(timeout);
236
+ reject(new Error(`Failed to start Appium: ${err.message}`));
237
+ });
238
+ this.appiumProcess.on('close', (code) => {
239
+ if (!started) {
240
+ clearTimeout(timeout);
241
+ reject(new Error(`Appium exited with code ${code}`));
242
+ }
243
+ });
244
+ });
245
+ }
246
+ /**
247
+ * Boot the iOS simulator
248
+ */
249
+ async bootSimulator(udid) {
250
+ try {
251
+ const devices = await this.simctl.getDevices();
252
+ let currentState;
253
+ // Find current device state
254
+ for (const deviceList of Object.values(devices)) {
255
+ if (!Array.isArray(deviceList))
256
+ continue;
257
+ const device = deviceList.find((d) => d.udid === udid);
258
+ if (device) {
259
+ currentState = device.state;
260
+ break;
261
+ }
262
+ }
263
+ if (currentState === 'Booted') {
264
+ return; // Already booted
265
+ }
266
+ // node-simctl expects udid to be set on the instance
267
+ this.simctl.udid = udid;
268
+ await this.simctl.bootDevice();
269
+ // Wait for device to be fully booted
270
+ let attempts = 0;
271
+ while (attempts < 60) {
272
+ const updatedDevices = await this.simctl.getDevices();
273
+ for (const deviceList of Object.values(updatedDevices)) {
274
+ if (!Array.isArray(deviceList))
275
+ continue;
276
+ const device = deviceList.find((d) => d.udid === udid);
277
+ if (device?.state === 'Booted') {
278
+ return;
279
+ }
280
+ }
281
+ await new Promise((r) => setTimeout(r, 1000));
282
+ attempts++;
283
+ }
284
+ throw new Error('Simulator failed to boot within 60 seconds');
285
+ }
286
+ catch (error) {
287
+ throw new Error(`Failed to boot simulator: ${error instanceof Error ? error.message : String(error)}`);
288
+ }
289
+ }
290
+ /**
291
+ * Launch iOS Safari via Appium
292
+ */
293
+ async launch(options = {}) {
294
+ if (this.isLaunched()) {
295
+ return; // Already launched
296
+ }
297
+ // Find device
298
+ let device = null;
299
+ if (options.udid) {
300
+ device = await this.findDevice(options.udid);
301
+ if (!device) {
302
+ throw new Error(`Device with UDID ${options.udid} not found`);
303
+ }
304
+ }
305
+ else if (options.device) {
306
+ device = await this.findDevice(options.device);
307
+ if (!device) {
308
+ throw new Error(`Device "${options.device}" not found. Run: agent-browser device list`);
309
+ }
310
+ }
311
+ else {
312
+ // Check environment variable
313
+ const envDevice = process.env.AGENT_BROWSER_IOS_DEVICE;
314
+ const envUdid = process.env.AGENT_BROWSER_IOS_UDID;
315
+ if (envUdid) {
316
+ device = await this.findDevice(envUdid);
317
+ if (!device) {
318
+ throw new Error(`Device with UDID ${envUdid} not found. Run: agent-browser device list`);
319
+ }
320
+ }
321
+ else if (envDevice) {
322
+ device = await this.findDevice(envDevice);
323
+ if (!device) {
324
+ throw new Error(`Device "${envDevice}" not found. Run: agent-browser device list`);
325
+ }
326
+ }
327
+ else {
328
+ device = await this.findDefaultDevice();
329
+ if (!device) {
330
+ throw new Error('No iOS simulators available. Open Xcode and download simulator runtimes.');
331
+ }
332
+ }
333
+ }
334
+ this.deviceUdid = device.udid;
335
+ this.deviceName = device.name;
336
+ // Start Appium server
337
+ await this.startAppiumServer();
338
+ // Boot simulator (skip for real devices - they're already running)
339
+ if (!device.isRealDevice) {
340
+ await this.bootSimulator(device.udid);
341
+ }
342
+ // Connect to Safari via Appium
343
+ try {
344
+ this.browser = await remote({
345
+ hostname: IOSManager.APPIUM_HOST,
346
+ port: IOSManager.APPIUM_PORT,
347
+ path: '/',
348
+ capabilities: {
349
+ platformName: 'iOS',
350
+ 'appium:automationName': 'XCUITest',
351
+ 'appium:deviceName': device.name,
352
+ 'appium:udid': device.udid,
353
+ browserName: 'Safari',
354
+ 'appium:noReset': true,
355
+ 'appium:newCommandTimeout': 300,
356
+ },
357
+ connectionRetryTimeout: 120000,
358
+ connectionRetryCount: 3,
359
+ });
360
+ }
361
+ catch (error) {
362
+ throw new Error(`Failed to connect to Safari: ${error instanceof Error ? error.message : String(error)}. ` +
363
+ 'Make sure XCUITest driver is installed: appium driver install xcuitest');
364
+ }
365
+ }
366
+ /**
367
+ * Navigate to URL
368
+ */
369
+ async navigate(url) {
370
+ if (!this.browser) {
371
+ throw new Error('iOS browser not launched. Call launch first.');
372
+ }
373
+ await this.browser.url(url);
374
+ // Wait for page to load
375
+ await this.browser.waitUntil(async () => {
376
+ const state = (await this.browser.execute('return document.readyState'));
377
+ return state === 'complete';
378
+ }, { timeout: 30000, interval: 500 });
379
+ const title = await this.browser.getTitle();
380
+ const currentUrl = await this.browser.getUrl();
381
+ return { url: currentUrl, title };
382
+ }
383
+ /**
384
+ * Get current URL
385
+ */
386
+ async getUrl() {
387
+ if (!this.browser) {
388
+ throw new Error('iOS browser not launched');
389
+ }
390
+ return this.browser.getUrl();
391
+ }
392
+ /**
393
+ * Get page title
394
+ */
395
+ async getTitle() {
396
+ if (!this.browser) {
397
+ throw new Error('iOS browser not launched');
398
+ }
399
+ return this.browser.getTitle();
400
+ }
401
+ /**
402
+ * Click/tap an element
403
+ */
404
+ async click(selector) {
405
+ if (!this.browser) {
406
+ throw new Error('iOS browser not launched');
407
+ }
408
+ const element = await this.getElement(selector);
409
+ await element.click();
410
+ }
411
+ /**
412
+ * Alias for click (semantic clarity for touch)
413
+ */
414
+ async tap(selector) {
415
+ return this.click(selector);
416
+ }
417
+ /**
418
+ * Type text into an element
419
+ */
420
+ async type(selector, text, options) {
421
+ if (!this.browser) {
422
+ throw new Error('iOS browser not launched');
423
+ }
424
+ const element = await this.getElement(selector);
425
+ if (options?.clear) {
426
+ await element.clearValue();
427
+ }
428
+ // WebdriverIO doesn't have a delay option, so we simulate it
429
+ if (options?.delay && options.delay > 0) {
430
+ for (const char of text) {
431
+ await element.addValue(char);
432
+ await new Promise((r) => setTimeout(r, options.delay));
433
+ }
434
+ }
435
+ else {
436
+ await element.addValue(text);
437
+ }
438
+ }
439
+ /**
440
+ * Fill an element (clear first, then type)
441
+ */
442
+ async fill(selector, value) {
443
+ if (!this.browser) {
444
+ throw new Error('iOS browser not launched');
445
+ }
446
+ const element = await this.getElement(selector);
447
+ await element.clearValue();
448
+ await element.setValue(value);
449
+ }
450
+ /**
451
+ * Get element by selector or ref
452
+ */
453
+ async getElement(selectorOrRef) {
454
+ if (!this.browser) {
455
+ throw new Error('iOS browser not launched');
456
+ }
457
+ // Check if it's a ref
458
+ const refData = this.getRefData(selectorOrRef);
459
+ if (refData) {
460
+ if (refData.xpath) {
461
+ return this.browser.$(refData.xpath);
462
+ }
463
+ return this.browser.$(refData.selector);
464
+ }
465
+ // Regular CSS selector
466
+ return this.browser.$(selectorOrRef);
467
+ }
468
+ /**
469
+ * Get ref data from ref string
470
+ */
471
+ getRefData(refArg) {
472
+ let ref = null;
473
+ if (refArg.startsWith('@')) {
474
+ ref = refArg.slice(1);
475
+ }
476
+ else if (refArg.startsWith('ref=')) {
477
+ ref = refArg.slice(4);
478
+ }
479
+ else if (/^e\d+$/.test(refArg)) {
480
+ ref = refArg;
481
+ }
482
+ if (ref && this.refMap[ref]) {
483
+ return this.refMap[ref];
484
+ }
485
+ return null;
486
+ }
487
+ /**
488
+ * Take a screenshot
489
+ */
490
+ async screenshot(options) {
491
+ if (!this.browser) {
492
+ throw new Error('iOS browser not launched');
493
+ }
494
+ const base64 = await this.browser.takeScreenshot();
495
+ if (options?.path) {
496
+ const { writeFileSync, mkdirSync } = await import('node:fs');
497
+ const dir = path.dirname(options.path);
498
+ if (!existsSync(dir)) {
499
+ mkdirSync(dir, { recursive: true });
500
+ }
501
+ writeFileSync(options.path, base64, 'base64');
502
+ return { path: options.path };
503
+ }
504
+ return { base64 };
505
+ }
506
+ /**
507
+ * Get page snapshot with refs
508
+ */
509
+ async getSnapshot(options) {
510
+ if (!this.browser) {
511
+ throw new Error('iOS browser not launched');
512
+ }
513
+ this.refCounter = 0;
514
+ this.refMap = {};
515
+ // Get page structure via JavaScript execution
516
+ // Note: The function runs in browser context, so we use 'any' for DOM types
517
+ const snapshot = await this.browser.execute(function (interactiveOnly) {
518
+ const INTERACTIVE_ROLES = new Set([
519
+ 'button',
520
+ 'link',
521
+ 'textbox',
522
+ 'checkbox',
523
+ 'radio',
524
+ 'combobox',
525
+ 'listbox',
526
+ 'menuitem',
527
+ 'option',
528
+ 'searchbox',
529
+ 'slider',
530
+ 'spinbutton',
531
+ 'switch',
532
+ 'tab',
533
+ 'treeitem',
534
+ ]);
535
+ const INTERACTIVE_TAGS = new Set([
536
+ 'A',
537
+ 'BUTTON',
538
+ 'INPUT',
539
+ 'SELECT',
540
+ 'TEXTAREA',
541
+ 'DETAILS',
542
+ 'SUMMARY',
543
+ ]);
544
+ function getXPath(element) {
545
+ if (element.id) {
546
+ return `//*[@id="${element.id}"]`;
547
+ }
548
+ const parts = [];
549
+ let current = element;
550
+ while (current && current.nodeType === 1) {
551
+ // Node.ELEMENT_NODE = 1
552
+ let index = 1;
553
+ let sibling = current.previousElementSibling;
554
+ while (sibling) {
555
+ if (sibling.nodeName === current.nodeName) {
556
+ index++;
557
+ }
558
+ sibling = sibling.previousElementSibling;
559
+ }
560
+ const tagName = current.nodeName.toLowerCase();
561
+ parts.unshift(`${tagName}[${index}]`);
562
+ current = current.parentElement;
563
+ }
564
+ return '/' + parts.join('/');
565
+ }
566
+ function getAccessibleName(element) {
567
+ // aria-label takes precedence
568
+ const ariaLabel = element.getAttribute('aria-label');
569
+ if (ariaLabel)
570
+ return ariaLabel;
571
+ // For inputs, check placeholder and label
572
+ const tagName = element.tagName;
573
+ if (tagName === 'INPUT' || tagName === 'TEXTAREA') {
574
+ const id = element.id;
575
+ if (id) {
576
+ const label = document.querySelector(`label[for="${id}"]`);
577
+ if (label)
578
+ return label.textContent?.trim() || '';
579
+ }
580
+ if (element.placeholder)
581
+ return element.placeholder;
582
+ }
583
+ // For buttons and links, use text content
584
+ if (tagName === 'BUTTON' || tagName === 'A') {
585
+ return element.textContent?.trim() || '';
586
+ }
587
+ // aria-labelledby
588
+ const labelledBy = element.getAttribute('aria-labelledby');
589
+ if (labelledBy) {
590
+ const labelElement = document.getElementById(labelledBy);
591
+ if (labelElement)
592
+ return labelElement.textContent?.trim() || '';
593
+ }
594
+ return element.textContent?.trim().slice(0, 50) || '';
595
+ }
596
+ function getRole(element) {
597
+ // Explicit role
598
+ const role = element.getAttribute('role');
599
+ if (role)
600
+ return role;
601
+ // Implicit roles
602
+ const tag = element.tagName;
603
+ if (tag === 'A' && element.hasAttribute('href'))
604
+ return 'link';
605
+ if (tag === 'BUTTON')
606
+ return 'button';
607
+ if (tag === 'INPUT') {
608
+ const type = element.type;
609
+ if (type === 'checkbox')
610
+ return 'checkbox';
611
+ if (type === 'radio')
612
+ return 'radio';
613
+ if (type === 'text' || type === 'email' || type === 'password' || type === 'search')
614
+ return 'textbox';
615
+ if (type === 'submit' || type === 'button')
616
+ return 'button';
617
+ }
618
+ if (tag === 'TEXTAREA')
619
+ return 'textbox';
620
+ if (tag === 'SELECT')
621
+ return 'combobox';
622
+ if (tag === 'H1' ||
623
+ tag === 'H2' ||
624
+ tag === 'H3' ||
625
+ tag === 'H4' ||
626
+ tag === 'H5' ||
627
+ tag === 'H6')
628
+ return 'heading';
629
+ if (tag === 'IMG')
630
+ return 'img';
631
+ if (tag === 'NAV')
632
+ return 'navigation';
633
+ if (tag === 'MAIN')
634
+ return 'main';
635
+ if (tag === 'HEADER')
636
+ return 'banner';
637
+ if (tag === 'FOOTER')
638
+ return 'contentinfo';
639
+ return null;
640
+ }
641
+ function traverse(element, depth) {
642
+ if (depth > 10)
643
+ return null; // Limit depth
644
+ const tag = element.tagName;
645
+ const role = getRole(element);
646
+ const name = getAccessibleName(element);
647
+ const isInteractive = INTERACTIVE_TAGS.has(tag) || (role !== null && INTERACTIVE_ROLES.has(role));
648
+ // Skip hidden elements
649
+ const style = window.getComputedStyle(element);
650
+ if (style.display === 'none' || style.visibility === 'hidden') {
651
+ return null;
652
+ }
653
+ const children = [];
654
+ for (const child of element.children) {
655
+ const childInfo = traverse(child, depth + 1);
656
+ if (childInfo) {
657
+ children.push(childInfo);
658
+ }
659
+ }
660
+ // In interactive mode, skip non-interactive elements without interactive children
661
+ if (interactiveOnly && !isInteractive && children.length === 0) {
662
+ return null;
663
+ }
664
+ return {
665
+ tag,
666
+ role,
667
+ name,
668
+ text: element.textContent?.trim().slice(0, 100) || '',
669
+ isInteractive,
670
+ xpath: getXPath(element),
671
+ children,
672
+ };
673
+ }
674
+ const root = traverse(document.body, 0);
675
+ return root;
676
+ }, options?.interactive ?? false);
677
+ // Build tree string and refs
678
+ const lines = [];
679
+ const buildTree = (node, indent) => {
680
+ if (!node)
681
+ return;
682
+ const prefix = ' '.repeat(indent) + '- ';
683
+ const role = node.role || node.tag.toLowerCase();
684
+ const name = node.name;
685
+ let line = `${prefix}${role}`;
686
+ if (name) {
687
+ line += ` "${name}"`;
688
+ }
689
+ // Add ref for interactive elements
690
+ if (node.isInteractive) {
691
+ const ref = `e${++this.refCounter}`;
692
+ line += ` [ref=${ref}]`;
693
+ this.refMap[ref] = {
694
+ selector: node.xpath.startsWith('/') ? node.xpath : `#${node.xpath}`,
695
+ role: node.role,
696
+ name: node.name,
697
+ xpath: node.xpath,
698
+ };
699
+ }
700
+ lines.push(line);
701
+ for (const child of node.children || []) {
702
+ buildTree(child, indent + 1);
703
+ }
704
+ };
705
+ if (snapshot) {
706
+ buildTree(snapshot, 0);
707
+ }
708
+ const tree = lines.join('\n') || '(empty)';
709
+ this.lastSnapshot = tree;
710
+ return { tree, refs: this.refMap };
711
+ }
712
+ /**
713
+ * Get cached ref map
714
+ */
715
+ getRefMap() {
716
+ return this.refMap;
717
+ }
718
+ /**
719
+ * Scroll the page
720
+ */
721
+ async scroll(options) {
722
+ if (!this.browser) {
723
+ throw new Error('iOS browser not launched');
724
+ }
725
+ const amount = options?.amount ?? 300;
726
+ if (options?.selector) {
727
+ const element = await this.getElement(options.selector);
728
+ await element.scrollIntoView();
729
+ return;
730
+ }
731
+ // Use JavaScript scrolling
732
+ let deltaX = options?.x ?? 0;
733
+ let deltaY = options?.y ?? 0;
734
+ if (options?.direction) {
735
+ switch (options.direction) {
736
+ case 'up':
737
+ deltaY = -amount;
738
+ break;
739
+ case 'down':
740
+ deltaY = amount;
741
+ break;
742
+ case 'left':
743
+ deltaX = -amount;
744
+ break;
745
+ case 'right':
746
+ deltaX = amount;
747
+ break;
748
+ }
749
+ }
750
+ await this.browser.execute(function (x, y) {
751
+ window.scrollBy(x, y);
752
+ }, deltaX, deltaY);
753
+ }
754
+ /**
755
+ * Swipe gesture (iOS-specific)
756
+ */
757
+ async swipe(direction, options) {
758
+ if (!this.browser) {
759
+ throw new Error('iOS browser not launched');
760
+ }
761
+ const distance = options?.distance ?? 300;
762
+ // Map direction to scroll (opposite direction)
763
+ const scrollDirection = {
764
+ up: 'down',
765
+ down: 'up',
766
+ left: 'right',
767
+ right: 'left',
768
+ }[direction];
769
+ await this.scroll({ direction: scrollDirection, amount: distance });
770
+ }
771
+ /**
772
+ * Execute JavaScript
773
+ */
774
+ async evaluate(script, ...args) {
775
+ if (!this.browser) {
776
+ throw new Error('iOS browser not launched');
777
+ }
778
+ // Execute the script directly - WebdriverIO handles the context
779
+ const result = await this.browser.execute(function (code, evalArgs) {
780
+ // Create a function from the code and execute it
781
+ const fn = new Function(...evalArgs.map((_, i) => `arg${i}`), code);
782
+ return fn(...evalArgs);
783
+ }, script.includes('return') ? script : `return (${script})`, args);
784
+ return result;
785
+ }
786
+ /**
787
+ * Wait for element
788
+ */
789
+ async wait(options) {
790
+ if (!this.browser) {
791
+ throw new Error('iOS browser not launched');
792
+ }
793
+ const timeout = options.timeout ?? 30000;
794
+ if (options.selector) {
795
+ const element = await this.getElement(options.selector);
796
+ switch (options.state) {
797
+ case 'detached':
798
+ await element.waitForExist({ timeout, reverse: true });
799
+ break;
800
+ case 'hidden':
801
+ await element.waitForDisplayed({ timeout, reverse: true });
802
+ break;
803
+ case 'visible':
804
+ await element.waitForDisplayed({ timeout });
805
+ break;
806
+ case 'attached':
807
+ default:
808
+ await element.waitForExist({ timeout });
809
+ break;
810
+ }
811
+ }
812
+ else {
813
+ // Just wait for timeout
814
+ await new Promise((r) => setTimeout(r, timeout));
815
+ }
816
+ }
817
+ /**
818
+ * Press a key
819
+ */
820
+ async press(key) {
821
+ if (!this.browser) {
822
+ throw new Error('iOS browser not launched');
823
+ }
824
+ // Map common key names
825
+ const keyMap = {
826
+ Enter: '\uE007',
827
+ Tab: '\uE004',
828
+ Escape: '\uE00C',
829
+ Backspace: '\uE003',
830
+ Delete: '\uE017',
831
+ ArrowUp: '\uE013',
832
+ ArrowDown: '\uE015',
833
+ ArrowLeft: '\uE012',
834
+ ArrowRight: '\uE014',
835
+ };
836
+ const mappedKey = keyMap[key] ?? key;
837
+ await this.browser.keys(mappedKey);
838
+ }
839
+ /**
840
+ * Hover over element (limited on touch - just scrolls into view)
841
+ */
842
+ async hover(selector) {
843
+ if (!this.browser) {
844
+ throw new Error('iOS browser not launched');
845
+ }
846
+ const element = await this.getElement(selector);
847
+ await element.scrollIntoView();
848
+ }
849
+ /**
850
+ * Get page content (HTML)
851
+ */
852
+ async getContent(selector) {
853
+ if (!this.browser) {
854
+ throw new Error('iOS browser not launched');
855
+ }
856
+ if (selector) {
857
+ const element = await this.getElement(selector);
858
+ return element.getHTML();
859
+ }
860
+ return this.browser.getPageSource();
861
+ }
862
+ /**
863
+ * Get text content of element
864
+ */
865
+ async getText(selector) {
866
+ if (!this.browser) {
867
+ throw new Error('iOS browser not launched');
868
+ }
869
+ const element = await this.getElement(selector);
870
+ return element.getText();
871
+ }
872
+ /**
873
+ * Get attribute value
874
+ */
875
+ async getAttribute(selector, attribute) {
876
+ if (!this.browser) {
877
+ throw new Error('iOS browser not launched');
878
+ }
879
+ const element = await this.getElement(selector);
880
+ return element.getAttribute(attribute);
881
+ }
882
+ /**
883
+ * Check if element is visible
884
+ */
885
+ async isVisible(selector) {
886
+ if (!this.browser) {
887
+ throw new Error('iOS browser not launched');
888
+ }
889
+ try {
890
+ const element = await this.getElement(selector);
891
+ return element.isDisplayed();
892
+ }
893
+ catch {
894
+ return false;
895
+ }
896
+ }
897
+ /**
898
+ * Check if element is enabled
899
+ */
900
+ async isEnabled(selector) {
901
+ if (!this.browser) {
902
+ throw new Error('iOS browser not launched');
903
+ }
904
+ const element = await this.getElement(selector);
905
+ return element.isEnabled();
906
+ }
907
+ /**
908
+ * Navigate back
909
+ */
910
+ async goBack() {
911
+ if (!this.browser) {
912
+ throw new Error('iOS browser not launched');
913
+ }
914
+ await this.browser.back();
915
+ }
916
+ /**
917
+ * Navigate forward
918
+ */
919
+ async goForward() {
920
+ if (!this.browser) {
921
+ throw new Error('iOS browser not launched');
922
+ }
923
+ await this.browser.forward();
924
+ }
925
+ /**
926
+ * Reload page
927
+ */
928
+ async reload() {
929
+ if (!this.browser) {
930
+ throw new Error('iOS browser not launched');
931
+ }
932
+ await this.browser.refresh();
933
+ }
934
+ /**
935
+ * Select option(s) from dropdown
936
+ */
937
+ async select(selector, values) {
938
+ if (!this.browser) {
939
+ throw new Error('iOS browser not launched');
940
+ }
941
+ const element = await this.getElement(selector);
942
+ const valueArray = Array.isArray(values) ? values : [values];
943
+ for (const value of valueArray) {
944
+ await element.selectByAttribute('value', value);
945
+ }
946
+ }
947
+ /**
948
+ * Check a checkbox
949
+ */
950
+ async check(selector) {
951
+ if (!this.browser) {
952
+ throw new Error('iOS browser not launched');
953
+ }
954
+ const element = await this.getElement(selector);
955
+ const isChecked = await element.isSelected();
956
+ if (!isChecked) {
957
+ await element.click();
958
+ }
959
+ }
960
+ /**
961
+ * Uncheck a checkbox
962
+ */
963
+ async uncheck(selector) {
964
+ if (!this.browser) {
965
+ throw new Error('iOS browser not launched');
966
+ }
967
+ const element = await this.getElement(selector);
968
+ const isChecked = await element.isSelected();
969
+ if (isChecked) {
970
+ await element.click();
971
+ }
972
+ }
973
+ /**
974
+ * Focus an element
975
+ */
976
+ async focus(selector) {
977
+ if (!this.browser) {
978
+ throw new Error('iOS browser not launched');
979
+ }
980
+ const element = await this.getElement(selector);
981
+ await this.browser.execute(function (el) {
982
+ el.focus();
983
+ }, element);
984
+ }
985
+ /**
986
+ * Clear input field
987
+ */
988
+ async clear(selector) {
989
+ if (!this.browser) {
990
+ throw new Error('iOS browser not launched');
991
+ }
992
+ const element = await this.getElement(selector);
993
+ await element.clearValue();
994
+ }
995
+ /**
996
+ * Get element count
997
+ */
998
+ async count(selector) {
999
+ if (!this.browser) {
1000
+ throw new Error('iOS browser not launched');
1001
+ }
1002
+ const elements = await this.browser.$$(selector);
1003
+ return elements.length;
1004
+ }
1005
+ /**
1006
+ * Get bounding box
1007
+ */
1008
+ async getBoundingBox(selector) {
1009
+ if (!this.browser) {
1010
+ throw new Error('iOS browser not launched');
1011
+ }
1012
+ try {
1013
+ const element = await this.getElement(selector);
1014
+ const location = await element.getLocation();
1015
+ const size = await element.getSize();
1016
+ return {
1017
+ x: location.x,
1018
+ y: location.y,
1019
+ width: size.width,
1020
+ height: size.height,
1021
+ };
1022
+ }
1023
+ catch {
1024
+ return null;
1025
+ }
1026
+ }
1027
+ /**
1028
+ * Get device info
1029
+ */
1030
+ getDeviceInfo() {
1031
+ if (!this.deviceName || !this.deviceUdid) {
1032
+ return null;
1033
+ }
1034
+ return {
1035
+ name: this.deviceName,
1036
+ udid: this.deviceUdid,
1037
+ };
1038
+ }
1039
+ /**
1040
+ * Close browser and cleanup
1041
+ */
1042
+ async close() {
1043
+ if (this.browser) {
1044
+ try {
1045
+ await this.browser.deleteSession();
1046
+ }
1047
+ catch {
1048
+ // Ignore cleanup errors
1049
+ }
1050
+ this.browser = null;
1051
+ }
1052
+ if (this.appiumProcess) {
1053
+ this.appiumProcess.kill();
1054
+ this.appiumProcess = null;
1055
+ }
1056
+ // Optionally shutdown simulator
1057
+ if (this.deviceUdid) {
1058
+ try {
1059
+ this.simctl.udid = this.deviceUdid;
1060
+ await this.simctl.shutdownDevice();
1061
+ }
1062
+ catch {
1063
+ // Ignore - simulator might already be shutdown
1064
+ }
1065
+ }
1066
+ this.deviceUdid = null;
1067
+ this.deviceName = null;
1068
+ this.refMap = {};
1069
+ this.lastSnapshot = '';
1070
+ this.refCounter = 0;
1071
+ }
1072
+ }
1073
+ //# sourceMappingURL=ios-manager.js.map