visor-ai 0.2.5 → 0.2.6

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) {
@@ -421,90 +435,7 @@ export class RealAppiumAdapter {
421
435
  await driver.releaseActions();
422
436
  }
423
437
  }
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) {
438
+ export async function getAdapter(platform, serverUrl = DEFAULT_SERVER_URL, device, appId, attachToRunning = false) {
505
439
  const normalized = platform.toLowerCase();
506
- if (useMock) {
507
- return new MockAdapter(normalized);
508
- }
509
440
  return RealAppiumAdapter.create(normalized, serverUrl, device, appId, attachToRunning);
510
441
  }
@@ -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
  }