visor-ai 0.2.5 → 0.2.7

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
@@ -29,7 +29,6 @@ Supported today:
29
29
  - Android
30
30
  - iOS
31
31
  - real Appium-backed runs
32
- - mock runs for predictable dry-run behavior
33
32
 
34
33
  ## Install
35
34
 
package/dist/adapters.js CHANGED
@@ -1,14 +1,9 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { remote } from 'webdriverio';
4
3
  import { errorMessage, parseServerUrl, sleep } from './utils.js';
5
4
  export const DEFAULT_SERVER_URL = 'http://127.0.0.1:4723';
6
5
  export const DEFAULT_ANDROID_APP = 'com.example.app';
7
6
  export const DEFAULT_IOS_BUNDLE = 'com.example.app';
8
- export const DEFAULT_ANDROID_DEVICE = 'emulator-5554';
9
- export const DEFAULT_IOS_DEVICE = 'iPhone 17 Pro';
10
- const MINI_PNG_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO7Z0x8AAAAASUVORK5CYII=';
11
- const MINI_PNG = Buffer.from(MINI_PNG_BASE64, 'base64');
12
7
  export const ACCESSIBILITY_ID = 'accessibility id';
13
8
  export const XPATH = 'xpath';
14
9
  export const ELEMENT_ID = 'id';
@@ -25,6 +20,18 @@ function envBool(preferred, legacy, defaultValue = false) {
25
20
  }
26
21
  return ['1', 'true', 'yes', 'on'].includes(raw.trim().toLowerCase());
27
22
  }
23
+ function envNumber(preferred, defaultValue) {
24
+ const raw = process.env[preferred];
25
+ if (raw === undefined) {
26
+ return defaultValue;
27
+ }
28
+ const parsed = Number(raw);
29
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultValue;
30
+ }
31
+ export function resolveWebdriverConnectionTimeout(platform) {
32
+ const defaultTimeout = platform === 'ios' ? 240000 : 60000;
33
+ return envNumber('VISOR_WEBDRIVER_CONNECTION_TIMEOUT_MS', defaultTimeout);
34
+ }
28
35
  function pngDimensions(filePath) {
29
36
  try {
30
37
  const header = fs.readFileSync(filePath).subarray(0, 24);
@@ -50,6 +57,9 @@ export function formatDriverCreationError(platform, appId, attachToRunning, erro
50
57
  : '';
51
58
  return `Failed to create WebdriverIO Appium session: ${message}. On iOS, --app-id must be the exact installed bundle identifier for the target app (${targetApp}). Android package names do not carry over automatically.${attachHint}`;
52
59
  }
60
+ if (platform === 'ios' && (message.includes('CoreSimulatorService') || message.includes('simctl'))) {
61
+ return `Failed to create WebdriverIO Appium session: ${message}. iOS simulator access failed before the command ran; verify \`xcrun simctl list\` works from the same shell and that Appium is not running in a sandboxed process.`;
62
+ }
53
63
  return `Failed to create WebdriverIO Appium session: ${message}`;
54
64
  }
55
65
  export function parseTarget(target) {
@@ -297,11 +307,13 @@ export class RealAppiumAdapter {
297
307
  envBool('VISOR_ATTACH_TO_RUNNING', 'PATF_ATTACH_TO_RUNNING', false);
298
308
  const server = parseServerUrl(this.serverUrl);
299
309
  const capabilities = {};
310
+ if (!this.device) {
311
+ throw new Error('A running device must be selected before creating an Appium session.');
312
+ }
300
313
  if (this.platform === 'android') {
301
314
  capabilities.platformName = 'Android';
302
315
  capabilities['appium:automationName'] = 'UiAutomator2';
303
- capabilities['appium:udid'] =
304
- this.device ?? env('VISOR_ANDROID_DEVICE', 'PATF_ANDROID_DEVICE', DEFAULT_ANDROID_DEVICE);
316
+ capabilities['appium:udid'] = this.device;
305
317
  capabilities['appium:appPackage'] =
306
318
  this.appId ?? env('VISOR_ANDROID_APP_PACKAGE', 'PATF_ANDROID_APP_PACKAGE', DEFAULT_ANDROID_APP);
307
319
  capabilities['appium:appActivity'] = env('VISOR_ANDROID_APP_ACTIVITY', 'PATF_ANDROID_APP_ACTIVITY', '.MainActivity');
@@ -316,8 +328,7 @@ export class RealAppiumAdapter {
316
328
  else {
317
329
  capabilities.platformName = 'iOS';
318
330
  capabilities['appium:automationName'] = 'XCUITest';
319
- capabilities['appium:deviceName'] =
320
- this.device ?? env('VISOR_IOS_DEVICE', 'PATF_IOS_DEVICE', DEFAULT_IOS_DEVICE);
331
+ capabilities['appium:udid'] = this.device;
321
332
  capabilities['appium:bundleId'] =
322
333
  this.appId ?? env('VISOR_IOS_BUNDLE_ID', 'PATF_IOS_BUNDLE_ID', DEFAULT_IOS_BUNDLE);
323
334
  capabilities['appium:newCommandTimeout'] = 60;
@@ -330,13 +341,16 @@ export class RealAppiumAdapter {
330
341
  }
331
342
  }
332
343
  try {
344
+ const { remote } = await import('webdriverio');
333
345
  return await remote({
334
346
  protocol: server.protocol,
335
347
  hostname: server.host,
336
348
  port: server.port,
337
349
  path: server.pathname,
338
350
  capabilities,
339
- logLevel: 'error'
351
+ logLevel: 'error',
352
+ connectionRetryCount: envNumber('VISOR_WEBDRIVER_CONNECTION_RETRY_COUNT', 0),
353
+ connectionRetryTimeout: resolveWebdriverConnectionTimeout(this.platform)
340
354
  });
341
355
  }
342
356
  catch (error) {
@@ -359,11 +373,11 @@ export class RealAppiumAdapter {
359
373
  async tapPoint(x, y) {
360
374
  const driver = this.requireDriver();
361
375
  if (this.platform === 'android') {
362
- await driver.execute('mobile: clickGesture', [{ x, y }]);
376
+ await driver.execute('mobile: clickGesture', { x, y });
363
377
  return;
364
378
  }
365
379
  if (this.platform === 'ios') {
366
- await driver.execute('mobile: tap', [{ x, y }]);
380
+ await driver.execute('mobile: tap', { x, y });
367
381
  return;
368
382
  }
369
383
  throw new Error(`Coordinate tap is unsupported for platform: ${this.platform}`);
@@ -376,16 +390,26 @@ export class RealAppiumAdapter {
376
390
  const width = Math.max(1, Math.round(size.width * 0.8));
377
391
  const height = Math.max(1, Math.round(size.height * 0.8));
378
392
  if (this.platform === 'android') {
379
- await driver.execute('mobile: scrollGesture', [
380
- { left, top, width, height, direction, percent: gesturePercent }
381
- ]);
393
+ await driver.execute('mobile: scrollGesture', {
394
+ left,
395
+ top,
396
+ width,
397
+ height,
398
+ direction,
399
+ percent: gesturePercent
400
+ });
382
401
  return;
383
402
  }
384
403
  if (this.platform === 'ios') {
385
404
  try {
386
- await driver.execute('mobile: scrollGesture', [
387
- { left, top, width, height, direction, percent: gesturePercent }
388
- ]);
405
+ await driver.execute('mobile: scrollGesture', {
406
+ left,
407
+ top,
408
+ width,
409
+ height,
410
+ direction,
411
+ percent: gesturePercent
412
+ });
389
413
  return;
390
414
  }
391
415
  catch {
@@ -421,90 +445,7 @@ export class RealAppiumAdapter {
421
445
  await driver.releaseActions();
422
446
  }
423
447
  }
424
- export class MockAdapter {
425
- platform;
426
- constructor(platform) {
427
- this.platform = platform;
428
- }
429
- capability() {
430
- return {
431
- platform: this.platform,
432
- commands: ['navigate', 'tap', 'act', 'scroll', 'screenshot', 'wait', 'source']
433
- };
434
- }
435
- async navigate(args) {
436
- return { action: 'navigate', platform: this.platform, args };
437
- }
438
- async tap(args) {
439
- resolveTapMode(args);
440
- return { action: 'tap', platform: this.platform, args };
441
- }
442
- async act(args) {
443
- return { action: 'act', platform: this.platform, args };
444
- }
445
- async scroll(args) {
446
- const { direction, percent } = resolveScrollOptions(args);
447
- return {
448
- action: 'scroll',
449
- platform: this.platform,
450
- args: { direction, percent }
451
- };
452
- }
453
- async screenshot(args) {
454
- const label = String(args.label ?? 'capture');
455
- const filePath = path.resolve(typeof args.path === 'string' ? args.path : `${label}.png`);
456
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
457
- fs.writeFileSync(filePath, MINI_PNG);
458
- const { width, height } = pngDimensions(filePath);
459
- return {
460
- action: 'screenshot',
461
- platform: this.platform,
462
- args: {
463
- label,
464
- file: path.basename(filePath),
465
- path: filePath,
466
- width,
467
- height
468
- }
469
- };
470
- }
471
- async wait(args) {
472
- const ms = Number(args.ms ?? 0);
473
- if (ms < 0) {
474
- throw new Error('wait requires non-negative ms');
475
- }
476
- return { action: 'wait', platform: this.platform, args: { ms } };
477
- }
478
- async source(args) {
479
- const label = String(args.label ?? 'source');
480
- const filePath = path.resolve(typeof args.path === 'string' ? args.path : `${label}.xml`);
481
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
482
- const content = `<hierarchy platform="${this.platform}"><node text="mock" /></hierarchy>\n`;
483
- fs.writeFileSync(filePath, content, 'utf8');
484
- return {
485
- action: 'source',
486
- platform: this.platform,
487
- args: {
488
- label,
489
- file: path.basename(filePath),
490
- path: filePath,
491
- format: 'xml',
492
- bytes: fs.statSync(filePath).size
493
- }
494
- };
495
- }
496
- async exists(target) {
497
- const lowered = target.toLowerCase();
498
- return !lowered.includes('missing') && !lowered.includes('not_found');
499
- }
500
- async close() {
501
- return;
502
- }
503
- }
504
- export async function getAdapter(platform, serverUrl = DEFAULT_SERVER_URL, device, useMock = false, appId, attachToRunning = false) {
448
+ export async function getAdapter(platform, serverUrl = DEFAULT_SERVER_URL, device, appId, attachToRunning = false) {
505
449
  const normalized = platform.toLowerCase();
506
- if (useMock) {
507
- return new MockAdapter(normalized);
508
- }
509
450
  return RealAppiumAdapter.create(normalized, serverUrl, device, appId, attachToRunning);
510
451
  }
@@ -25,7 +25,10 @@ function pidExists(pid) {
25
25
  process.kill(pid, 0);
26
26
  return true;
27
27
  }
28
- catch {
28
+ catch (error) {
29
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'EPERM') {
30
+ return true;
31
+ }
29
32
  return false;
30
33
  }
31
34
  }