visor-ai 0.2.4 → 0.2.5
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 -1
- package/dist/adapters.js +87 -2
- package/dist/cli.js +7 -1
- package/dist/runner.js +2 -0
- package/dist/validator.js +19 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -42,4 +42,16 @@ visor --help
|
|
|
42
42
|
|
|
43
43
|
## Documentation
|
|
44
44
|
|
|
45
|
-
Comprehensive product documentation lives in [the docs site](https://
|
|
45
|
+
Comprehensive product documentation lives in [the docs site](https://www.visorai.dev/docs).
|
|
46
|
+
|
|
47
|
+
## Releases
|
|
48
|
+
|
|
49
|
+
Create a release by running one of:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm run release:patch
|
|
53
|
+
npm run release:minor
|
|
54
|
+
npm run release:major
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
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
|
@@ -115,6 +115,22 @@ export function resolveTapMode(args) {
|
|
|
115
115
|
}
|
|
116
116
|
throw new Error('tap requires target or x/y coordinates');
|
|
117
117
|
}
|
|
118
|
+
function resolveScrollOptions(args) {
|
|
119
|
+
const direction = typeof args.direction === 'string' ? args.direction.toLowerCase() : '';
|
|
120
|
+
if (direction !== 'up' && direction !== 'down') {
|
|
121
|
+
throw new Error("scroll requires args.direction to be 'up' or 'down'");
|
|
122
|
+
}
|
|
123
|
+
const rawPercent = args.percent ?? 70;
|
|
124
|
+
const percent = Number(rawPercent);
|
|
125
|
+
if (!Number.isFinite(percent) || percent < 1 || percent > 100) {
|
|
126
|
+
throw new Error('scroll args.percent must be a number between 1 and 100');
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
direction,
|
|
130
|
+
percent,
|
|
131
|
+
gesturePercent: percent / 100
|
|
132
|
+
};
|
|
133
|
+
}
|
|
118
134
|
export class RealAppiumAdapter {
|
|
119
135
|
platform;
|
|
120
136
|
serverUrl;
|
|
@@ -137,7 +153,7 @@ export class RealAppiumAdapter {
|
|
|
137
153
|
capability() {
|
|
138
154
|
return {
|
|
139
155
|
platform: this.platform,
|
|
140
|
-
commands: ['navigate', 'tap', 'act', 'screenshot', 'wait', 'source']
|
|
156
|
+
commands: ['navigate', 'tap', 'act', 'scroll', 'screenshot', 'wait', 'source']
|
|
141
157
|
};
|
|
142
158
|
}
|
|
143
159
|
async navigate(args) {
|
|
@@ -200,6 +216,15 @@ export class RealAppiumAdapter {
|
|
|
200
216
|
}
|
|
201
217
|
throw new Error('Unsupported act operation; use --name type --target <selector> --value <text> or --name back');
|
|
202
218
|
}
|
|
219
|
+
async scroll(args) {
|
|
220
|
+
const { direction, percent, gesturePercent } = resolveScrollOptions(args);
|
|
221
|
+
await this.scrollViewport(direction, gesturePercent);
|
|
222
|
+
return {
|
|
223
|
+
action: 'scroll',
|
|
224
|
+
platform: this.platform,
|
|
225
|
+
args: { direction, percent }
|
|
226
|
+
};
|
|
227
|
+
}
|
|
203
228
|
async screenshot(args) {
|
|
204
229
|
const label = String(args.label ?? 'capture');
|
|
205
230
|
const filePath = path.resolve(typeof args.path === 'string' ? args.path : `${label}.png`);
|
|
@@ -343,6 +368,58 @@ export class RealAppiumAdapter {
|
|
|
343
368
|
}
|
|
344
369
|
throw new Error(`Coordinate tap is unsupported for platform: ${this.platform}`);
|
|
345
370
|
}
|
|
371
|
+
async scrollViewport(direction, gesturePercent) {
|
|
372
|
+
const driver = this.requireDriver();
|
|
373
|
+
const size = await driver.getWindowSize();
|
|
374
|
+
const left = Math.max(0, Math.round(size.width * 0.1));
|
|
375
|
+
const top = Math.max(0, Math.round(size.height * 0.1));
|
|
376
|
+
const width = Math.max(1, Math.round(size.width * 0.8));
|
|
377
|
+
const height = Math.max(1, Math.round(size.height * 0.8));
|
|
378
|
+
if (this.platform === 'android') {
|
|
379
|
+
await driver.execute('mobile: scrollGesture', [
|
|
380
|
+
{ left, top, width, height, direction, percent: gesturePercent }
|
|
381
|
+
]);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (this.platform === 'ios') {
|
|
385
|
+
try {
|
|
386
|
+
await driver.execute('mobile: scrollGesture', [
|
|
387
|
+
{ left, top, width, height, direction, percent: gesturePercent }
|
|
388
|
+
]);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
await this.swipeViewport(direction, gesturePercent, size.width, size.height);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
throw new Error(`Scroll is unsupported for platform: ${this.platform}`);
|
|
397
|
+
}
|
|
398
|
+
async swipeViewport(direction, gesturePercent, viewportWidth, viewportHeight) {
|
|
399
|
+
const driver = this.requireDriver();
|
|
400
|
+
const x = Math.round(viewportWidth / 2);
|
|
401
|
+
const lowY = Math.round(viewportHeight * 0.75);
|
|
402
|
+
const highY = Math.round(viewportHeight * 0.25);
|
|
403
|
+
const travel = Math.max(1, Math.round(viewportHeight * gesturePercent));
|
|
404
|
+
const startY = direction === 'down' ? lowY : highY;
|
|
405
|
+
const unclampedEndY = direction === 'down' ? startY - travel : startY + travel;
|
|
406
|
+
const endY = Math.max(1, Math.min(viewportHeight - 1, unclampedEndY));
|
|
407
|
+
await driver.performActions([
|
|
408
|
+
{
|
|
409
|
+
type: 'pointer',
|
|
410
|
+
id: 'finger1',
|
|
411
|
+
parameters: { pointerType: 'touch' },
|
|
412
|
+
actions: [
|
|
413
|
+
{ type: 'pointerMove', duration: 0, x, y: startY },
|
|
414
|
+
{ type: 'pointerDown', button: 0 },
|
|
415
|
+
{ type: 'pause', duration: 150 },
|
|
416
|
+
{ type: 'pointerMove', duration: 400, x, y: endY },
|
|
417
|
+
{ type: 'pointerUp', button: 0 }
|
|
418
|
+
]
|
|
419
|
+
}
|
|
420
|
+
]);
|
|
421
|
+
await driver.releaseActions();
|
|
422
|
+
}
|
|
346
423
|
}
|
|
347
424
|
export class MockAdapter {
|
|
348
425
|
platform;
|
|
@@ -352,7 +429,7 @@ export class MockAdapter {
|
|
|
352
429
|
capability() {
|
|
353
430
|
return {
|
|
354
431
|
platform: this.platform,
|
|
355
|
-
commands: ['navigate', 'tap', 'act', 'screenshot', 'wait', 'source']
|
|
432
|
+
commands: ['navigate', 'tap', 'act', 'scroll', 'screenshot', 'wait', 'source']
|
|
356
433
|
};
|
|
357
434
|
}
|
|
358
435
|
async navigate(args) {
|
|
@@ -365,6 +442,14 @@ export class MockAdapter {
|
|
|
365
442
|
async act(args) {
|
|
366
443
|
return { action: 'act', platform: this.platform, args };
|
|
367
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
|
+
}
|
|
368
453
|
async screenshot(args) {
|
|
369
454
|
const label = String(args.label ?? 'capture');
|
|
370
455
|
const filePath = path.resolve(typeof args.path === 'string' ? args.path : `${label}.png`);
|
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ const ACTION_COMMANDS = new Set([
|
|
|
9
9
|
'tap',
|
|
10
10
|
'navigate',
|
|
11
11
|
'act',
|
|
12
|
+
'scroll',
|
|
12
13
|
'screenshot',
|
|
13
14
|
'wait',
|
|
14
15
|
'source'
|
|
@@ -44,6 +45,8 @@ const ACTION_SPEC = {
|
|
|
44
45
|
target: 'string',
|
|
45
46
|
x: 'number',
|
|
46
47
|
y: 'number',
|
|
48
|
+
direction: 'string',
|
|
49
|
+
percent: 'number',
|
|
47
50
|
normalized: 'boolean',
|
|
48
51
|
to: 'string',
|
|
49
52
|
name: 'string',
|
|
@@ -103,6 +106,7 @@ const COMMAND_SPECS = {
|
|
|
103
106
|
tap: ACTION_SPEC,
|
|
104
107
|
navigate: ACTION_SPEC,
|
|
105
108
|
act: ACTION_SPEC,
|
|
109
|
+
scroll: ACTION_SPEC,
|
|
106
110
|
screenshot: ACTION_SPEC,
|
|
107
111
|
wait: ACTION_SPEC,
|
|
108
112
|
source: ACTION_SPEC
|
|
@@ -123,11 +127,12 @@ function helpText() {
|
|
|
123
127
|
' start [--server-url <url>]',
|
|
124
128
|
' status [--server-url <url>]',
|
|
125
129
|
' stop [--server-url <url>] [--force]',
|
|
126
|
-
' tap|navigate|act|screenshot|wait|source',
|
|
130
|
+
' tap|navigate|act|scroll|screenshot|wait|source',
|
|
127
131
|
'',
|
|
128
132
|
'Examples:',
|
|
129
133
|
' visor validate scenarios/checkout-smoke.json',
|
|
130
134
|
' visor run scenarios/checkout-smoke.json --mock --output artifacts-test',
|
|
135
|
+
' visor scroll --platform android --mock --direction down',
|
|
131
136
|
' node dist/main.js status'
|
|
132
137
|
].join('\n');
|
|
133
138
|
}
|
|
@@ -288,6 +293,7 @@ function cmdHelp() {
|
|
|
288
293
|
examples: [
|
|
289
294
|
'visor validate scenarios/checkout-smoke.json',
|
|
290
295
|
'visor run scenarios/checkout-smoke.json --mock --output artifacts-test',
|
|
296
|
+
'visor scroll --platform android --mock --direction down',
|
|
291
297
|
'node dist/main.js status'
|
|
292
298
|
]
|
|
293
299
|
};
|
package/dist/runner.js
CHANGED
package/dist/validator.js
CHANGED
|
@@ -5,7 +5,7 @@ const ALLOWED_CONFIG = new Set(['timeoutMs', 'seed', 'artifactsDir']);
|
|
|
5
5
|
const ALLOWED_STEP = new Set(['id', 'command', 'args']);
|
|
6
6
|
const ALLOWED_ASSERT = new Set(['id', 'type', 'target']);
|
|
7
7
|
const ALLOWED_OUTPUT = new Set(['report']);
|
|
8
|
-
const SUPPORTED_COMMANDS = new Set(['tap', 'navigate', 'act', 'screenshot', 'wait', 'source']);
|
|
8
|
+
const SUPPORTED_COMMANDS = new Set(['tap', 'navigate', 'act', 'scroll', 'screenshot', 'wait', 'source']);
|
|
9
9
|
function validationIssue(severity, code, message, issuePath) {
|
|
10
10
|
return { severity, code, message, path: issuePath };
|
|
11
11
|
}
|
|
@@ -46,6 +46,21 @@ function validateTapArgs(args, issuePath) {
|
|
|
46
46
|
}
|
|
47
47
|
return issues;
|
|
48
48
|
}
|
|
49
|
+
function validateScrollArgs(args, issuePath) {
|
|
50
|
+
const issues = [];
|
|
51
|
+
if (!Object.hasOwn(args, 'direction')) {
|
|
52
|
+
issues.push(validationIssue('error', 'ARG_ERROR', 'scroll requires args.direction', issuePath));
|
|
53
|
+
return issues;
|
|
54
|
+
}
|
|
55
|
+
if (typeof args.direction !== 'string' || !['up', 'down'].includes(args.direction.toLowerCase())) {
|
|
56
|
+
issues.push(validationIssue('error', 'ARG_ERROR', "scroll args.direction must be 'up' or 'down'", issuePath));
|
|
57
|
+
}
|
|
58
|
+
if (Object.hasOwn(args, 'percent') &&
|
|
59
|
+
(typeof args.percent !== 'number' || !Number.isFinite(args.percent) || args.percent < 1 || args.percent > 100)) {
|
|
60
|
+
issues.push(validationIssue('error', 'ARG_ERROR', 'scroll args.percent must be a number between 1 and 100', issuePath));
|
|
61
|
+
}
|
|
62
|
+
return issues;
|
|
63
|
+
}
|
|
49
64
|
export function parseAndValidate(filePath) {
|
|
50
65
|
const issues = [];
|
|
51
66
|
const raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
@@ -115,6 +130,9 @@ export function parseAndValidate(filePath) {
|
|
|
115
130
|
if (command === 'navigate' && isRecord(args) && !Object.hasOwn(args, 'to')) {
|
|
116
131
|
issues.push(validationIssue('error', 'ARG_ERROR', 'navigate requires args.to', `${issuePath}.args`));
|
|
117
132
|
}
|
|
133
|
+
if (command === 'scroll' && isRecord(args)) {
|
|
134
|
+
issues.push(...validateScrollArgs(args, `${issuePath}.args`));
|
|
135
|
+
}
|
|
118
136
|
if (command === 'screenshot' && isRecord(args) && !Object.hasOwn(args, 'label')) {
|
|
119
137
|
issues.push(validationIssue('warning', 'DETERMINISM_WARNING', 'screenshot missing label may reduce determinism', `${issuePath}.args`));
|
|
120
138
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "visor-ai",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Visor CLI for LLM-driven mobile app interaction and artifact capture",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,11 @@
|
|
|
30
30
|
"build": "rm -rf dist && tsc -p tsconfig.json",
|
|
31
31
|
"dev": "tsx src/main.ts",
|
|
32
32
|
"test": "vitest run",
|
|
33
|
-
"check": "npm run build && npm run test"
|
|
33
|
+
"check": "npm run build && npm run test",
|
|
34
|
+
"release": "node scripts/release.mjs",
|
|
35
|
+
"release:patch": "node scripts/release.mjs patch",
|
|
36
|
+
"release:minor": "node scripts/release.mjs minor",
|
|
37
|
+
"release:major": "node scripts/release.mjs major"
|
|
34
38
|
},
|
|
35
39
|
"keywords": [
|
|
36
40
|
"appium",
|