visor-ai 0.2.4 → 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 +13 -2
- package/dist/adapters.js +98 -82
- package/dist/appiumLifecycle.js +4 -1
- package/dist/cli.js +168 -192
- package/dist/daemon.js +517 -0
- package/dist/devices.js +123 -0
- package/dist/main.js +5 -0
- package/dist/runner.js +7 -3
- package/dist/validator.js +25 -6
- package/package.json +7 -2
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
|
|
|
@@ -42,4 +41,16 @@ visor --help
|
|
|
42
41
|
|
|
43
42
|
## Documentation
|
|
44
43
|
|
|
45
|
-
Comprehensive product documentation lives in [the docs site](https://
|
|
44
|
+
Comprehensive product documentation lives in [the docs site](https://www.visorai.dev/docs).
|
|
45
|
+
|
|
46
|
+
## Releases
|
|
47
|
+
|
|
48
|
+
Create a release by running one of:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm run release:patch
|
|
52
|
+
npm run release:minor
|
|
53
|
+
npm run release:major
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Each command runs `npm run check`, bumps the version, creates the matching `vX.Y.Z` git tag, and pushes the commit and tag upstream with `git push --follow-tags`.
|
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) {
|
|
@@ -115,6 +125,22 @@ export function resolveTapMode(args) {
|
|
|
115
125
|
}
|
|
116
126
|
throw new Error('tap requires target or x/y coordinates');
|
|
117
127
|
}
|
|
128
|
+
function resolveScrollOptions(args) {
|
|
129
|
+
const direction = typeof args.direction === 'string' ? args.direction.toLowerCase() : '';
|
|
130
|
+
if (direction !== 'up' && direction !== 'down') {
|
|
131
|
+
throw new Error("scroll requires args.direction to be 'up' or 'down'");
|
|
132
|
+
}
|
|
133
|
+
const rawPercent = args.percent ?? 70;
|
|
134
|
+
const percent = Number(rawPercent);
|
|
135
|
+
if (!Number.isFinite(percent) || percent < 1 || percent > 100) {
|
|
136
|
+
throw new Error('scroll args.percent must be a number between 1 and 100');
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
direction,
|
|
140
|
+
percent,
|
|
141
|
+
gesturePercent: percent / 100
|
|
142
|
+
};
|
|
143
|
+
}
|
|
118
144
|
export class RealAppiumAdapter {
|
|
119
145
|
platform;
|
|
120
146
|
serverUrl;
|
|
@@ -137,7 +163,7 @@ export class RealAppiumAdapter {
|
|
|
137
163
|
capability() {
|
|
138
164
|
return {
|
|
139
165
|
platform: this.platform,
|
|
140
|
-
commands: ['navigate', 'tap', 'act', 'screenshot', 'wait', 'source']
|
|
166
|
+
commands: ['navigate', 'tap', 'act', 'scroll', 'screenshot', 'wait', 'source']
|
|
141
167
|
};
|
|
142
168
|
}
|
|
143
169
|
async navigate(args) {
|
|
@@ -200,6 +226,15 @@ export class RealAppiumAdapter {
|
|
|
200
226
|
}
|
|
201
227
|
throw new Error('Unsupported act operation; use --name type --target <selector> --value <text> or --name back');
|
|
202
228
|
}
|
|
229
|
+
async scroll(args) {
|
|
230
|
+
const { direction, percent, gesturePercent } = resolveScrollOptions(args);
|
|
231
|
+
await this.scrollViewport(direction, gesturePercent);
|
|
232
|
+
return {
|
|
233
|
+
action: 'scroll',
|
|
234
|
+
platform: this.platform,
|
|
235
|
+
args: { direction, percent }
|
|
236
|
+
};
|
|
237
|
+
}
|
|
203
238
|
async screenshot(args) {
|
|
204
239
|
const label = String(args.label ?? 'capture');
|
|
205
240
|
const filePath = path.resolve(typeof args.path === 'string' ? args.path : `${label}.png`);
|
|
@@ -272,11 +307,13 @@ export class RealAppiumAdapter {
|
|
|
272
307
|
envBool('VISOR_ATTACH_TO_RUNNING', 'PATF_ATTACH_TO_RUNNING', false);
|
|
273
308
|
const server = parseServerUrl(this.serverUrl);
|
|
274
309
|
const capabilities = {};
|
|
310
|
+
if (!this.device) {
|
|
311
|
+
throw new Error('A running device must be selected before creating an Appium session.');
|
|
312
|
+
}
|
|
275
313
|
if (this.platform === 'android') {
|
|
276
314
|
capabilities.platformName = 'Android';
|
|
277
315
|
capabilities['appium:automationName'] = 'UiAutomator2';
|
|
278
|
-
capabilities['appium:udid'] =
|
|
279
|
-
this.device ?? env('VISOR_ANDROID_DEVICE', 'PATF_ANDROID_DEVICE', DEFAULT_ANDROID_DEVICE);
|
|
316
|
+
capabilities['appium:udid'] = this.device;
|
|
280
317
|
capabilities['appium:appPackage'] =
|
|
281
318
|
this.appId ?? env('VISOR_ANDROID_APP_PACKAGE', 'PATF_ANDROID_APP_PACKAGE', DEFAULT_ANDROID_APP);
|
|
282
319
|
capabilities['appium:appActivity'] = env('VISOR_ANDROID_APP_ACTIVITY', 'PATF_ANDROID_APP_ACTIVITY', '.MainActivity');
|
|
@@ -291,8 +328,7 @@ export class RealAppiumAdapter {
|
|
|
291
328
|
else {
|
|
292
329
|
capabilities.platformName = 'iOS';
|
|
293
330
|
capabilities['appium:automationName'] = 'XCUITest';
|
|
294
|
-
capabilities['appium:
|
|
295
|
-
this.device ?? env('VISOR_IOS_DEVICE', 'PATF_IOS_DEVICE', DEFAULT_IOS_DEVICE);
|
|
331
|
+
capabilities['appium:udid'] = this.device;
|
|
296
332
|
capabilities['appium:bundleId'] =
|
|
297
333
|
this.appId ?? env('VISOR_IOS_BUNDLE_ID', 'PATF_IOS_BUNDLE_ID', DEFAULT_IOS_BUNDLE);
|
|
298
334
|
capabilities['appium:newCommandTimeout'] = 60;
|
|
@@ -305,13 +341,16 @@ export class RealAppiumAdapter {
|
|
|
305
341
|
}
|
|
306
342
|
}
|
|
307
343
|
try {
|
|
344
|
+
const { remote } = await import('webdriverio');
|
|
308
345
|
return await remote({
|
|
309
346
|
protocol: server.protocol,
|
|
310
347
|
hostname: server.host,
|
|
311
348
|
port: server.port,
|
|
312
349
|
path: server.pathname,
|
|
313
350
|
capabilities,
|
|
314
|
-
logLevel: 'error'
|
|
351
|
+
logLevel: 'error',
|
|
352
|
+
connectionRetryCount: envNumber('VISOR_WEBDRIVER_CONNECTION_RETRY_COUNT', 0),
|
|
353
|
+
connectionRetryTimeout: resolveWebdriverConnectionTimeout(this.platform)
|
|
315
354
|
});
|
|
316
355
|
}
|
|
317
356
|
catch (error) {
|
|
@@ -343,83 +382,60 @@ export class RealAppiumAdapter {
|
|
|
343
382
|
}
|
|
344
383
|
throw new Error(`Coordinate tap is unsupported for platform: ${this.platform}`);
|
|
345
384
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const label = String(args.label ?? 'capture');
|
|
370
|
-
const filePath = path.resolve(typeof args.path === 'string' ? args.path : `${label}.png`);
|
|
371
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
372
|
-
fs.writeFileSync(filePath, MINI_PNG);
|
|
373
|
-
const { width, height } = pngDimensions(filePath);
|
|
374
|
-
return {
|
|
375
|
-
action: 'screenshot',
|
|
376
|
-
platform: this.platform,
|
|
377
|
-
args: {
|
|
378
|
-
label,
|
|
379
|
-
file: path.basename(filePath),
|
|
380
|
-
path: filePath,
|
|
381
|
-
width,
|
|
382
|
-
height
|
|
385
|
+
async scrollViewport(direction, gesturePercent) {
|
|
386
|
+
const driver = this.requireDriver();
|
|
387
|
+
const size = await driver.getWindowSize();
|
|
388
|
+
const left = Math.max(0, Math.round(size.width * 0.1));
|
|
389
|
+
const top = Math.max(0, Math.round(size.height * 0.1));
|
|
390
|
+
const width = Math.max(1, Math.round(size.width * 0.8));
|
|
391
|
+
const height = Math.max(1, Math.round(size.height * 0.8));
|
|
392
|
+
if (this.platform === 'android') {
|
|
393
|
+
await driver.execute('mobile: scrollGesture', [
|
|
394
|
+
{ left, top, width, height, direction, percent: gesturePercent }
|
|
395
|
+
]);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (this.platform === 'ios') {
|
|
399
|
+
try {
|
|
400
|
+
await driver.execute('mobile: scrollGesture', [
|
|
401
|
+
{ left, top, width, height, direction, percent: gesturePercent }
|
|
402
|
+
]);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
await this.swipeViewport(direction, gesturePercent, size.width, size.height);
|
|
407
|
+
return;
|
|
383
408
|
}
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
async wait(args) {
|
|
387
|
-
const ms = Number(args.ms ?? 0);
|
|
388
|
-
if (ms < 0) {
|
|
389
|
-
throw new Error('wait requires non-negative ms');
|
|
390
409
|
}
|
|
391
|
-
|
|
410
|
+
throw new Error(`Scroll is unsupported for platform: ${this.platform}`);
|
|
392
411
|
}
|
|
393
|
-
async
|
|
394
|
-
const
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
412
|
+
async swipeViewport(direction, gesturePercent, viewportWidth, viewportHeight) {
|
|
413
|
+
const driver = this.requireDriver();
|
|
414
|
+
const x = Math.round(viewportWidth / 2);
|
|
415
|
+
const lowY = Math.round(viewportHeight * 0.75);
|
|
416
|
+
const highY = Math.round(viewportHeight * 0.25);
|
|
417
|
+
const travel = Math.max(1, Math.round(viewportHeight * gesturePercent));
|
|
418
|
+
const startY = direction === 'down' ? lowY : highY;
|
|
419
|
+
const unclampedEndY = direction === 'down' ? startY - travel : startY + travel;
|
|
420
|
+
const endY = Math.max(1, Math.min(viewportHeight - 1, unclampedEndY));
|
|
421
|
+
await driver.performActions([
|
|
422
|
+
{
|
|
423
|
+
type: 'pointer',
|
|
424
|
+
id: 'finger1',
|
|
425
|
+
parameters: { pointerType: 'touch' },
|
|
426
|
+
actions: [
|
|
427
|
+
{ type: 'pointerMove', duration: 0, x, y: startY },
|
|
428
|
+
{ type: 'pointerDown', button: 0 },
|
|
429
|
+
{ type: 'pause', duration: 150 },
|
|
430
|
+
{ type: 'pointerMove', duration: 400, x, y: endY },
|
|
431
|
+
{ type: 'pointerUp', button: 0 }
|
|
432
|
+
]
|
|
408
433
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
async exists(target) {
|
|
412
|
-
const lowered = target.toLowerCase();
|
|
413
|
-
return !lowered.includes('missing') && !lowered.includes('not_found');
|
|
414
|
-
}
|
|
415
|
-
async close() {
|
|
416
|
-
return;
|
|
434
|
+
]);
|
|
435
|
+
await driver.releaseActions();
|
|
417
436
|
}
|
|
418
437
|
}
|
|
419
|
-
export async function getAdapter(platform, serverUrl = DEFAULT_SERVER_URL, device,
|
|
438
|
+
export async function getAdapter(platform, serverUrl = DEFAULT_SERVER_URL, device, appId, attachToRunning = false) {
|
|
420
439
|
const normalized = platform.toLowerCase();
|
|
421
|
-
if (useMock) {
|
|
422
|
-
return new MockAdapter(normalized);
|
|
423
|
-
}
|
|
424
440
|
return RealAppiumAdapter.create(normalized, serverUrl, device, appId, attachToRunning);
|
|
425
441
|
}
|
package/dist/appiumLifecycle.js
CHANGED