visor-ai 0.2.7 → 0.2.9
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/dist/adapters.js +224 -22
- package/dist/appMap.js +2486 -0
- package/dist/cli.js +424 -45
- package/dist/daemon.js +87 -11
- package/dist/localRuntime.js +124 -0
- package/dist/runner.js +25 -2
- package/package.json +4 -1
package/dist/adapters.js
CHANGED
|
@@ -10,6 +10,7 @@ export const ELEMENT_ID = 'id';
|
|
|
10
10
|
export const ANDROID_UIAUTOMATOR = '-android uiautomator';
|
|
11
11
|
export const IOS_PREDICATE = '-ios predicate string';
|
|
12
12
|
export const IOS_CLASS_CHAIN = '-ios class chain';
|
|
13
|
+
const TEXT_ATTRIBUTES = ['text', 'content-desc', 'label', 'name', 'value'];
|
|
13
14
|
function env(preferred, legacy, defaultValue) {
|
|
14
15
|
return process.env[preferred] ?? process.env[legacy] ?? defaultValue;
|
|
15
16
|
}
|
|
@@ -48,6 +49,38 @@ function pngDimensions(filePath) {
|
|
|
48
49
|
return { width: null, height: null };
|
|
49
50
|
}
|
|
50
51
|
}
|
|
52
|
+
function validateRect(rect) {
|
|
53
|
+
const candidate = rect && typeof rect === 'object' && !Array.isArray(rect)
|
|
54
|
+
? rect
|
|
55
|
+
: {};
|
|
56
|
+
const x = Number(candidate.x);
|
|
57
|
+
const y = Number(candidate.y);
|
|
58
|
+
const width = Number(candidate.width);
|
|
59
|
+
const height = Number(candidate.height);
|
|
60
|
+
if (![x, y, width, height].every(Number.isFinite) || width <= 0 || height <= 0) {
|
|
61
|
+
throw new Error('Element rectangle is invalid');
|
|
62
|
+
}
|
|
63
|
+
return { x, y, width, height };
|
|
64
|
+
}
|
|
65
|
+
function xpathLiteral(value) {
|
|
66
|
+
if (!value.includes("'")) {
|
|
67
|
+
return `'${value}'`;
|
|
68
|
+
}
|
|
69
|
+
if (!value.includes('"')) {
|
|
70
|
+
return `"${value}"`;
|
|
71
|
+
}
|
|
72
|
+
return `concat(${value
|
|
73
|
+
.split("'")
|
|
74
|
+
.map((part) => `'${part}'`)
|
|
75
|
+
.join(', "\"\'\"", ')})`;
|
|
76
|
+
}
|
|
77
|
+
function textXPath(value, mode) {
|
|
78
|
+
const literal = xpathLiteral(value);
|
|
79
|
+
const clauses = TEXT_ATTRIBUTES.map((attribute) => mode === 'exact'
|
|
80
|
+
? `@${attribute} = ${literal}`
|
|
81
|
+
: `contains(@${attribute}, ${literal})`);
|
|
82
|
+
return `//*[${clauses.join(' or ')}]`;
|
|
83
|
+
}
|
|
51
84
|
export function formatDriverCreationError(platform, appId, attachToRunning, error) {
|
|
52
85
|
const message = errorMessage(error);
|
|
53
86
|
if (platform === 'ios' && message.includes('bundle identifier') && message.includes('unknown')) {
|
|
@@ -63,28 +96,30 @@ export function formatDriverCreationError(platform, appId, attachToRunning, erro
|
|
|
63
96
|
return `Failed to create WebdriverIO Appium session: ${message}`;
|
|
64
97
|
}
|
|
65
98
|
export function parseTarget(target) {
|
|
99
|
+
const valueAfterPrefix = () => target.slice(target.indexOf('=') + 1);
|
|
100
|
+
if (target.startsWith('text~=')) {
|
|
101
|
+
return [XPATH, textXPath(valueAfterPrefix(), 'contains')];
|
|
102
|
+
}
|
|
66
103
|
if (target.startsWith('text=')) {
|
|
67
|
-
|
|
68
|
-
const xpath = `//*[contains(@text, '${value}') or contains(@content-desc, '${value}') or contains(@label, '${value}') or contains(@name, '${value}') or contains(@value, '${value}')]`;
|
|
69
|
-
return [XPATH, xpath];
|
|
104
|
+
return [XPATH, textXPath(valueAfterPrefix(), 'exact')];
|
|
70
105
|
}
|
|
71
106
|
if (target.startsWith('id=')) {
|
|
72
|
-
return [ELEMENT_ID,
|
|
107
|
+
return [ELEMENT_ID, valueAfterPrefix()];
|
|
73
108
|
}
|
|
74
109
|
if (target.startsWith('xpath=')) {
|
|
75
|
-
return [XPATH,
|
|
110
|
+
return [XPATH, valueAfterPrefix()];
|
|
76
111
|
}
|
|
77
112
|
if (target.startsWith('uiautomator=')) {
|
|
78
|
-
return [ANDROID_UIAUTOMATOR,
|
|
113
|
+
return [ANDROID_UIAUTOMATOR, valueAfterPrefix()];
|
|
79
114
|
}
|
|
80
115
|
if (target.startsWith('predicate=')) {
|
|
81
|
-
return [IOS_PREDICATE,
|
|
116
|
+
return [IOS_PREDICATE, valueAfterPrefix()];
|
|
82
117
|
}
|
|
83
118
|
if (target.startsWith('classchain=')) {
|
|
84
|
-
return [IOS_CLASS_CHAIN,
|
|
119
|
+
return [IOS_CLASS_CHAIN, valueAfterPrefix()];
|
|
85
120
|
}
|
|
86
121
|
if (target.startsWith('accessibility=')) {
|
|
87
|
-
return [ACCESSIBILITY_ID,
|
|
122
|
+
return [ACCESSIBILITY_ID, valueAfterPrefix()];
|
|
88
123
|
}
|
|
89
124
|
return [ACCESSIBILITY_ID, target];
|
|
90
125
|
}
|
|
@@ -194,7 +229,7 @@ export class RealAppiumAdapter {
|
|
|
194
229
|
const target = String(args.target);
|
|
195
230
|
const selector = selectorForTarget(target);
|
|
196
231
|
const element = await this.requireDriver().$(selector.selector);
|
|
197
|
-
await
|
|
232
|
+
await this.tapElementCenter(element);
|
|
198
233
|
return {
|
|
199
234
|
action: 'tap',
|
|
200
235
|
platform: this.platform,
|
|
@@ -224,7 +259,52 @@ export class RealAppiumAdapter {
|
|
|
224
259
|
args: { name }
|
|
225
260
|
};
|
|
226
261
|
}
|
|
227
|
-
|
|
262
|
+
if (name === 'drag') {
|
|
263
|
+
const gesture = await this.resolveDragGesture(args);
|
|
264
|
+
await this.dragPointer(gesture.start, gesture.end);
|
|
265
|
+
return {
|
|
266
|
+
action: 'act',
|
|
267
|
+
platform: this.platform,
|
|
268
|
+
args: {
|
|
269
|
+
name,
|
|
270
|
+
startX: gesture.start.x,
|
|
271
|
+
startY: gesture.start.y,
|
|
272
|
+
endX: gesture.end.x,
|
|
273
|
+
endY: gesture.end.y
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (name === 'slider') {
|
|
278
|
+
const gesture = await this.resolveSliderGesture(args);
|
|
279
|
+
await this.dragPointer(gesture.start, gesture.end);
|
|
280
|
+
return {
|
|
281
|
+
action: 'act',
|
|
282
|
+
platform: this.platform,
|
|
283
|
+
args: {
|
|
284
|
+
name,
|
|
285
|
+
target,
|
|
286
|
+
value: gesture.value,
|
|
287
|
+
startValue: gesture.startValue
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
if (name === 'home') {
|
|
292
|
+
await this.pressHome();
|
|
293
|
+
return {
|
|
294
|
+
action: 'act',
|
|
295
|
+
platform: this.platform,
|
|
296
|
+
args: { name }
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
if (name === 'reset') {
|
|
300
|
+
await this.resetApp();
|
|
301
|
+
return {
|
|
302
|
+
action: 'act',
|
|
303
|
+
platform: this.platform,
|
|
304
|
+
args: { name, app_id: this.appId }
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
throw new Error('Unsupported act operation; use --name type --target <selector> --value <text>, --name drag, --name slider, --name back, --name home, or --name reset');
|
|
228
308
|
}
|
|
229
309
|
async scroll(args) {
|
|
230
310
|
const { direction, percent, gesturePercent } = resolveScrollOptions(args);
|
|
@@ -382,6 +462,98 @@ export class RealAppiumAdapter {
|
|
|
382
462
|
}
|
|
383
463
|
throw new Error(`Coordinate tap is unsupported for platform: ${this.platform}`);
|
|
384
464
|
}
|
|
465
|
+
async tapElementCenter(element) {
|
|
466
|
+
try {
|
|
467
|
+
const rect = await this.elementRect(element);
|
|
468
|
+
const x = Math.round(rect.x + rect.width / 2);
|
|
469
|
+
const y = Math.round(rect.y + rect.height / 2);
|
|
470
|
+
await this.tapPoint(x, y);
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
await element.click();
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
async elementRect(element) {
|
|
477
|
+
if (typeof element.getRect === 'function') {
|
|
478
|
+
const rect = await element.getRect();
|
|
479
|
+
return validateRect(rect);
|
|
480
|
+
}
|
|
481
|
+
if (typeof element.getLocation === 'function' && typeof element.getSize === 'function') {
|
|
482
|
+
const location = await element.getLocation();
|
|
483
|
+
const size = await element.getSize();
|
|
484
|
+
return validateRect({
|
|
485
|
+
x: location.x,
|
|
486
|
+
y: location.y,
|
|
487
|
+
width: size.width,
|
|
488
|
+
height: size.height
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
throw new Error('Element rectangle is unavailable');
|
|
492
|
+
}
|
|
493
|
+
async pressHome() {
|
|
494
|
+
const driver = this.requireDriver();
|
|
495
|
+
if (this.platform === 'ios') {
|
|
496
|
+
await driver.execute('mobile: pressButton', { name: 'home' });
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
if (typeof driver.pressKeyCode === 'function') {
|
|
500
|
+
await driver.pressKeyCode(3);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
await driver.execute('mobile: pressKey', { keycode: 3 });
|
|
504
|
+
}
|
|
505
|
+
async resetApp() {
|
|
506
|
+
if (!this.appId) {
|
|
507
|
+
throw new Error('act reset requires --app-id so Visor can restart the target app');
|
|
508
|
+
}
|
|
509
|
+
const driver = this.requireDriver();
|
|
510
|
+
if (typeof driver.terminateApp === 'function') {
|
|
511
|
+
await driver.terminateApp(this.appId);
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
await driver.execute('mobile: terminateApp', this.platform === 'ios' ? { bundleId: this.appId } : { appId: this.appId });
|
|
515
|
+
}
|
|
516
|
+
await sleep(500);
|
|
517
|
+
if (typeof driver.activateApp === 'function') {
|
|
518
|
+
await driver.activateApp(this.appId);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
await driver.execute('mobile: activateApp', this.platform === 'ios' ? { bundleId: this.appId } : { appId: this.appId });
|
|
522
|
+
}
|
|
523
|
+
async resolveDragGesture(args) {
|
|
524
|
+
const normalized = Boolean(args.normalized);
|
|
525
|
+
const size = normalized ? await this.requireDriver().getWindowSize() : null;
|
|
526
|
+
const start = {
|
|
527
|
+
x: gestureNumber(args, ['startX', 'start-x', 'fromX', 'from-x'], 'act drag requires startX/startY and endX/endY'),
|
|
528
|
+
y: gestureNumber(args, ['startY', 'start-y', 'fromY', 'from-y'], 'act drag requires startX/startY and endX/endY')
|
|
529
|
+
};
|
|
530
|
+
const end = {
|
|
531
|
+
x: gestureNumber(args, ['endX', 'end-x', 'toX', 'to-x'], 'act drag requires startX/startY and endX/endY'),
|
|
532
|
+
y: gestureNumber(args, ['endY', 'end-y', 'toY', 'to-y'], 'act drag requires startX/startY and endX/endY')
|
|
533
|
+
};
|
|
534
|
+
return {
|
|
535
|
+
start: normalized && size ? scalePoint(start, size.width, size.height) : roundPoint(start),
|
|
536
|
+
end: normalized && size ? scalePoint(end, size.width, size.height) : roundPoint(end)
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
async resolveSliderGesture(args) {
|
|
540
|
+
const target = typeof args.target === 'string' ? args.target : '';
|
|
541
|
+
if (!target) {
|
|
542
|
+
throw new Error('act slider requires args.target');
|
|
543
|
+
}
|
|
544
|
+
const value = unitIntervalNumber(args.value, 'act slider args.value must be a number between 0 and 1');
|
|
545
|
+
const startValue = unitIntervalNumber(args.startValue ?? args['start-value'] ?? 0.5, 'act slider args.startValue must be a number between 0 and 1');
|
|
546
|
+
const selector = selectorForTarget(target);
|
|
547
|
+
const element = await this.requireDriver().$(selector.selector);
|
|
548
|
+
const rect = await this.elementRect(element);
|
|
549
|
+
const y = Math.round(rect.y + rect.height / 2);
|
|
550
|
+
return {
|
|
551
|
+
start: { x: Math.round(rect.x + rect.width * startValue), y },
|
|
552
|
+
end: { x: Math.round(rect.x + rect.width * value), y },
|
|
553
|
+
value,
|
|
554
|
+
startValue
|
|
555
|
+
};
|
|
556
|
+
}
|
|
385
557
|
async scrollViewport(direction, gesturePercent) {
|
|
386
558
|
const driver = this.requireDriver();
|
|
387
559
|
const size = await driver.getWindowSize();
|
|
@@ -419,31 +591,61 @@ export class RealAppiumAdapter {
|
|
|
419
591
|
}
|
|
420
592
|
throw new Error(`Scroll is unsupported for platform: ${this.platform}`);
|
|
421
593
|
}
|
|
422
|
-
async
|
|
594
|
+
async dragPointer(start, end) {
|
|
423
595
|
const driver = this.requireDriver();
|
|
424
|
-
const x = Math.round(viewportWidth / 2);
|
|
425
|
-
const lowY = Math.round(viewportHeight * 0.75);
|
|
426
|
-
const highY = Math.round(viewportHeight * 0.25);
|
|
427
|
-
const travel = Math.max(1, Math.round(viewportHeight * gesturePercent));
|
|
428
|
-
const startY = direction === 'down' ? lowY : highY;
|
|
429
|
-
const unclampedEndY = direction === 'down' ? startY - travel : startY + travel;
|
|
430
|
-
const endY = Math.max(1, Math.min(viewportHeight - 1, unclampedEndY));
|
|
431
596
|
await driver.performActions([
|
|
432
597
|
{
|
|
433
598
|
type: 'pointer',
|
|
434
599
|
id: 'finger1',
|
|
435
600
|
parameters: { pointerType: 'touch' },
|
|
436
601
|
actions: [
|
|
437
|
-
{ type: 'pointerMove', duration: 0, x, y:
|
|
602
|
+
{ type: 'pointerMove', duration: 0, x: start.x, y: start.y },
|
|
438
603
|
{ type: 'pointerDown', button: 0 },
|
|
439
|
-
{ type: 'pause', duration:
|
|
440
|
-
{ type: 'pointerMove', duration: 400, x, y:
|
|
604
|
+
{ type: 'pause', duration: 100 },
|
|
605
|
+
{ type: 'pointerMove', duration: 400, x: end.x, y: end.y },
|
|
441
606
|
{ type: 'pointerUp', button: 0 }
|
|
442
607
|
]
|
|
443
608
|
}
|
|
444
609
|
]);
|
|
445
610
|
await driver.releaseActions();
|
|
446
611
|
}
|
|
612
|
+
async swipeViewport(direction, gesturePercent, viewportWidth, viewportHeight) {
|
|
613
|
+
const x = Math.round(viewportWidth / 2);
|
|
614
|
+
const lowY = Math.round(viewportHeight * 0.75);
|
|
615
|
+
const highY = Math.round(viewportHeight * 0.25);
|
|
616
|
+
const travel = Math.max(1, Math.round(viewportHeight * gesturePercent));
|
|
617
|
+
const startY = direction === 'down' ? lowY : highY;
|
|
618
|
+
const unclampedEndY = direction === 'down' ? startY - travel : startY + travel;
|
|
619
|
+
const endY = Math.max(1, Math.min(viewportHeight - 1, unclampedEndY));
|
|
620
|
+
await this.dragPointer({ x, y: startY }, { x, y: endY });
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function gestureNumber(args, keys, message) {
|
|
624
|
+
const raw = keys.map((key) => args[key]).find((value) => value !== undefined);
|
|
625
|
+
const parsed = Number(raw);
|
|
626
|
+
if (!Number.isFinite(parsed)) {
|
|
627
|
+
throw new Error(message);
|
|
628
|
+
}
|
|
629
|
+
return parsed;
|
|
630
|
+
}
|
|
631
|
+
function unitIntervalNumber(value, message) {
|
|
632
|
+
const parsed = Number(value);
|
|
633
|
+
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
|
|
634
|
+
throw new Error(message);
|
|
635
|
+
}
|
|
636
|
+
return parsed;
|
|
637
|
+
}
|
|
638
|
+
function scalePoint(point, width, height) {
|
|
639
|
+
return {
|
|
640
|
+
x: Math.round(point.x * width),
|
|
641
|
+
y: Math.round(point.y * height)
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
function roundPoint(point) {
|
|
645
|
+
return {
|
|
646
|
+
x: Math.round(point.x),
|
|
647
|
+
y: Math.round(point.y)
|
|
648
|
+
};
|
|
447
649
|
}
|
|
448
650
|
export async function getAdapter(platform, serverUrl = DEFAULT_SERVER_URL, device, appId, attachToRunning = false) {
|
|
449
651
|
const normalized = platform.toLowerCase();
|