visor-ai 0.2.6 → 0.2.8

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/cli.js CHANGED
@@ -1,11 +1,22 @@
1
+ import fs from 'node:fs';
1
2
  import { DEFAULT_SERVER_URL } from './adapters.js';
2
- import { DaemonRequestTimeoutError, DaemonUnavailableError, runDaemonAction, runDaemonScenario, startVisorDaemon, statusVisorDaemon, stopVisorDaemon } from './daemon.js';
3
+ import { DaemonOperationError, DaemonRequestTimeoutError, DaemonUnavailableError, runDaemonAction, runDaemonDiscover, runDaemonScenario, startVisorDaemon, statusVisorDaemon, stopVisorDaemon } from './daemon.js';
3
4
  import { DeviceSelectionError, resolveRunningDevice } from './devices.js';
4
5
  import { makeError } from './errors.js';
6
+ import { LocalRuntimeAdapter } from './localRuntime.js';
5
7
  import { writeReports } from './report.js';
6
- import { determinismCheck } from './runner.js';
8
+ import { determinismCheck, runScenario } from './runner.js';
7
9
  import { errorMessage, makeId, utcNowIso } from './utils.js';
8
10
  import { parseAndValidate } from './validator.js';
11
+ function packageVersion() {
12
+ try {
13
+ const packageJson = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
14
+ return typeof packageJson.version === 'string' ? packageJson.version : 'unknown';
15
+ }
16
+ catch {
17
+ return 'unknown';
18
+ }
19
+ }
9
20
  const ACTION_COMMANDS = new Set([
10
21
  'tap',
11
22
  'navigate',
@@ -19,6 +30,7 @@ const ALL_COMMANDS = new Set([
19
30
  ...ACTION_COMMANDS,
20
31
  'validate',
21
32
  'run',
33
+ 'discover',
22
34
  'benchmark',
23
35
  'report',
24
36
  'start',
@@ -34,6 +46,7 @@ const GLOBAL_SPEC = {
34
46
  'server-url': 'string',
35
47
  'app-id': 'string',
36
48
  attach: 'boolean',
49
+ 'no-map': 'boolean',
37
50
  verbose: 'boolean'
38
51
  };
39
52
  const ACTION_SPEC = {
@@ -58,9 +71,20 @@ const COMMAND_SPECS = {
58
71
  timeout: 'number',
59
72
  output: 'string',
60
73
  format: 'string',
74
+ runtime: 'string',
75
+ 'server-url': 'string',
76
+ 'app-id': 'string',
77
+ attach: 'boolean',
78
+ 'no-map': 'boolean'
79
+ },
80
+ discover: {
81
+ device: 'string',
82
+ timeout: 'number',
83
+ format: 'string',
61
84
  'server-url': 'string',
62
85
  'app-id': 'string',
63
- attach: 'boolean'
86
+ attach: 'boolean',
87
+ 'no-map': 'boolean'
64
88
  },
65
89
  benchmark: {
66
90
  runs: 'number',
@@ -71,7 +95,8 @@ const COMMAND_SPECS = {
71
95
  format: 'string',
72
96
  'server-url': 'string',
73
97
  'app-id': 'string',
74
- attach: 'boolean'
98
+ attach: 'boolean',
99
+ 'no-map': 'boolean'
75
100
  },
76
101
  report: { format: 'string' },
77
102
  start: {
@@ -102,12 +127,14 @@ function helpText() {
102
127
  '',
103
128
  'Usage:',
104
129
  ' visor <command> [options]',
105
- ' visor --help',
130
+ ' visor --help | -h',
131
+ ' visor --version | -v',
106
132
  '',
107
133
  'Commands:',
108
134
  ' validate <scenario>',
109
- ' run <scenario> [--output <dir>]',
135
+ ' run <scenario> [--output <dir>] [--runtime appium|local]',
110
136
  ' benchmark <scenario> [--runs <n>] [--threshold <percent>]',
137
+ ' discover [--app-id <id>]',
111
138
  ' report [path]',
112
139
  ' start [--server-url <url>] [--appium-cmd <cmd>]',
113
140
  ' status [--server-url <url>]',
@@ -116,10 +143,13 @@ function helpText() {
116
143
  '',
117
144
  'Examples:',
118
145
  ' visor validate scenarios/checkout-smoke.json',
146
+ ' visor run path/to/scenario.json --runtime local --output artifacts-local',
119
147
  ' visor start --server-url http://127.0.0.1:4723',
120
148
  ' visor run scenarios/checkout-smoke.json --output artifacts-test',
149
+ ' visor run scenarios/checkout-smoke.json --no-map',
150
+ ' visor discover --app-id com.example.app',
121
151
  ' visor scroll --device emulator-5554 --direction down',
122
- ' node dist/main.js status'
152
+ ' visor status'
123
153
  ].join('\n');
124
154
  }
125
155
  function envelopeOk(commandId, startedAt, artifacts = [], nextAction = '') {
@@ -205,7 +235,10 @@ async function resolvedRuntime(options) {
205
235
  output_dir: String(options.output ?? 'artifacts'),
206
236
  server_url: String(options['server-url'] ?? DEFAULT_SERVER_URL),
207
237
  app_id: typeof options['app-id'] === 'string' ? options['app-id'] : undefined,
208
- attach_to_running: Boolean(options.attach)
238
+ attach_to_running: Boolean(options.attach),
239
+ map: {
240
+ enabled: !Boolean(options['no-map'])
241
+ }
209
242
  };
210
243
  }
211
244
  function actionArgs(command, options) {
@@ -217,8 +250,10 @@ function actionArgs(command, options) {
217
250
  'verbose',
218
251
  'server-url',
219
252
  'seed',
253
+ 'runtime',
220
254
  'app-id',
221
- 'attach'
255
+ 'attach',
256
+ 'no-map'
222
257
  ]);
223
258
  const args = Object.entries(options).reduce((acc, [key, value]) => {
224
259
  if (!commonIgnored.has(key) && value !== undefined) {
@@ -231,6 +266,10 @@ function actionArgs(command, options) {
231
266
  }
232
267
  return args;
233
268
  }
269
+ function unsupportedRuntimeResult(commandId, startedAt, runtime) {
270
+ const response = envelopeFail(commandId, startedAt, 'INPUT_ERROR', 'Unsupported runtime', `Unsupported runtime '${String(runtime)}'`, 'Use --runtime appium or --runtime local');
271
+ return { code: 1, response };
272
+ }
234
273
  function isTargetInitializationError(error) {
235
274
  const message = errorMessage(error);
236
275
  return (message.includes('Failed to create WebdriverIO Appium session') ||
@@ -259,14 +298,26 @@ function cmdHelp() {
259
298
  commands: Array.from(ALL_COMMANDS),
260
299
  examples: [
261
300
  'visor validate scenarios/checkout-smoke.json',
301
+ 'visor run path/to/scenario.json --runtime local --output artifacts-local',
262
302
  'visor start --server-url http://127.0.0.1:4723',
263
303
  'visor run scenarios/checkout-smoke.json --output artifacts-test',
264
304
  'visor scroll --device emulator-5554 --direction down',
265
- 'node dist/main.js status'
305
+ 'visor status'
266
306
  ]
267
307
  };
268
308
  return { code: 0, response };
269
309
  }
310
+ function cmdVersion() {
311
+ const commandId = makeId('cmd');
312
+ const startedAt = utcNowIso();
313
+ const version = packageVersion();
314
+ const response = envelopeOk(commandId, startedAt, [], 'none');
315
+ response.data = {
316
+ version,
317
+ versionText: version
318
+ };
319
+ return { code: 0, response };
320
+ }
270
321
  export async function cmdValidate(parsed) {
271
322
  const commandId = makeId('cmd');
272
323
  const startedAt = utcNowIso();
@@ -291,6 +342,19 @@ export async function cmdValidate(parsed) {
291
342
  return { code: 1, response };
292
343
  }
293
344
  }
345
+ async function cmdRunLocal(commandId, startedAt, scenario, warnings, runtime) {
346
+ const result = await runScenario(scenario, new LocalRuntimeAdapter(), runtime.device, runtime.timeout, runtime.output_dir);
347
+ const outputs = writeReports(result, runtime.output_dir);
348
+ const response = result.status === 'ok'
349
+ ? envelopeOk(commandId, startedAt, Object.values(outputs), 'report')
350
+ : envelopeFail(commandId, startedAt, result.error?.code ?? 'ACTION_ERROR', result.error?.message ?? 'Local runtime scenario failed', result.error?.likely_cause ?? 'The deterministic local runtime did not satisfy the scenario', result.error?.next_step ?? 'Inspect local runtime scenario artifacts and retry');
351
+ response.artifacts = Object.values(outputs);
352
+ response.data = {
353
+ run: result,
354
+ warnings
355
+ };
356
+ return { code: result.status === 'ok' ? 0 : 2, response };
357
+ }
294
358
  export async function cmdRun(parsed) {
295
359
  const commandId = makeId('cmd');
296
360
  const startedAt = utcNowIso();
@@ -301,6 +365,24 @@ export async function cmdRun(parsed) {
301
365
  response.data = { issues };
302
366
  return { code: 1, response };
303
367
  }
368
+ if (parsed.options.runtime !== undefined &&
369
+ parsed.options.runtime !== 'appium' &&
370
+ parsed.options.runtime !== 'local') {
371
+ return unsupportedRuntimeResult(commandId, startedAt, parsed.options.runtime);
372
+ }
373
+ if (parsed.options.runtime === 'local') {
374
+ const runtime = {
375
+ device: 'local',
376
+ timeout: typeof parsed.options.timeout === 'number'
377
+ ? parsed.options.timeout
378
+ : typeof scenario.config.timeoutMs === 'number'
379
+ ? scenario.config.timeoutMs
380
+ : undefined,
381
+ output_dir: String(parsed.options.output ??
382
+ (typeof scenario.config.artifactsDir === 'string' ? scenario.config.artifactsDir : 'artifacts'))
383
+ };
384
+ return cmdRunLocal(commandId, startedAt, scenario, warningIssues(issues), runtime);
385
+ }
304
386
  let runtime;
305
387
  try {
306
388
  runtime = await resolvedRuntime({
@@ -329,7 +411,10 @@ export async function cmdRun(parsed) {
329
411
  device: runtime.device,
330
412
  app_id: runtime.app_id,
331
413
  attach_to_running: runtime.attach_to_running
332
- }, scenario, runtime.device, runtime.timeout, runtime.output_dir);
414
+ }, scenario, runtime.device, runtime.timeout, runtime.output_dir, {
415
+ ...runtime.map,
416
+ appId: runtime.app_id
417
+ });
333
418
  const outputs = writeReports(result, runtime.output_dir);
334
419
  if (result.status === 'fail' && result.error) {
335
420
  const response = envelopeFail(commandId, startedAt, result.error.code, result.error.message, result.error.likely_cause, result.error.next_step);
@@ -362,6 +447,51 @@ export async function cmdRun(parsed) {
362
447
  return { code: 1, response };
363
448
  }
364
449
  }
450
+ export async function cmdDiscover(parsed) {
451
+ const commandId = makeId('cmd');
452
+ const startedAt = utcNowIso();
453
+ let runtime;
454
+ try {
455
+ runtime = await resolvedRuntime(parsed.options);
456
+ }
457
+ catch (error) {
458
+ if (!(error instanceof DeviceSelectionError)) {
459
+ throw error;
460
+ }
461
+ const response = envelopeFail(commandId, startedAt, 'TARGET_ERROR', 'Device selection failed', error.message, 'Start one target device or rerun with --device <device-id>');
462
+ response.data = { devices: error.devices };
463
+ return { code: 1, response };
464
+ }
465
+ try {
466
+ const result = await runDaemonDiscover({
467
+ platform: runtime.platform,
468
+ server_url: runtime.server_url,
469
+ device: runtime.device,
470
+ app_id: runtime.app_id,
471
+ attach_to_running: runtime.attach_to_running
472
+ }, {
473
+ ...runtime.map,
474
+ appId: runtime.app_id
475
+ });
476
+ const response = envelopeOk(commandId, startedAt, [], 'run');
477
+ response.data = result;
478
+ return { code: 0, response };
479
+ }
480
+ catch (error) {
481
+ const isDaemonUnavailable = error instanceof DaemonUnavailableError;
482
+ const isDaemonTimeout = error instanceof DaemonRequestTimeoutError;
483
+ const response = envelopeFail(commandId, startedAt, isDaemonUnavailable || isDaemonTimeout ? 'TARGET_ERROR' : 'ACTION_ERROR', isDaemonUnavailable
484
+ ? 'discover requires visor start'
485
+ : isDaemonTimeout
486
+ ? 'discover timed out waiting for Visor daemon'
487
+ : 'discover failed', errorMessage(error), isDaemonUnavailable
488
+ ? 'Run `visor start`, verify the target emulator/simulator is booted, and retry.'
489
+ : isDaemonTimeout
490
+ ? 'Inspect .visor/daemon/daemon.log and .visor/appium/*.log, then retry or run `visor stop --force` before restarting.'
491
+ : 'Check target app state and retry');
492
+ return { code: 1, response };
493
+ }
494
+ }
365
495
  export async function cmdBenchmark(parsed) {
366
496
  const commandId = makeId('cmd');
367
497
  const startedAt = utcNowIso();
@@ -406,7 +536,10 @@ export async function cmdBenchmark(parsed) {
406
536
  device: runtime.device,
407
537
  app_id: runtime.app_id,
408
538
  attach_to_running: runtime.attach_to_running
409
- }, scenario, runtime.device, runtime.timeout, runtime.output_dir);
539
+ }, scenario, runtime.device, runtime.timeout, runtime.output_dir, {
540
+ ...runtime.map,
541
+ appId: runtime.app_id
542
+ });
410
543
  writeReports(result, runtime.output_dir);
411
544
  signatures.push(result.determinism_signature);
412
545
  runIds.push(result.run_id);
@@ -477,7 +610,10 @@ export async function cmdAction(command, parsed) {
477
610
  device: options.device,
478
611
  app_id: options.app_id,
479
612
  attach_to_running: options.attach_to_running
480
- }, command, actionArgs(command, parsed.options));
613
+ }, command, actionArgs(command, parsed.options), {
614
+ ...options.map,
615
+ appId: options.app_id
616
+ });
481
617
  const actionPayload = payload.args;
482
618
  if (actionPayload && typeof actionPayload === 'object' && !Array.isArray(actionPayload)) {
483
619
  const maybePath = actionPayload.path;
@@ -487,6 +623,9 @@ export async function cmdAction(command, parsed) {
487
623
  }
488
624
  }
489
625
  catch (error) {
626
+ if (error instanceof DaemonOperationError && error.data) {
627
+ payload = error.data;
628
+ }
490
629
  actionError = error;
491
630
  }
492
631
  if (actionError) {
@@ -557,6 +696,9 @@ export async function executeCommand(argv) {
557
696
  argv.includes('-h')) {
558
697
  return cmdHelp();
559
698
  }
699
+ if (argv.includes('--version') || argv.includes('-v')) {
700
+ return cmdVersion();
701
+ }
560
702
  const parsed = parseCommand(argv);
561
703
  if (parsed.command === 'validate') {
562
704
  return cmdValidate(parsed);
@@ -564,6 +706,9 @@ export async function executeCommand(argv) {
564
706
  if (parsed.command === 'run') {
565
707
  return cmdRun(parsed);
566
708
  }
709
+ if (parsed.command === 'discover') {
710
+ return cmdDiscover(parsed);
711
+ }
567
712
  if (parsed.command === 'benchmark') {
568
713
  return cmdBenchmark(parsed);
569
714
  }
package/dist/daemon.js CHANGED
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import { spawn } from 'node:child_process';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { DEFAULT_SERVER_URL, getAdapter } from './adapters.js';
7
+ import { createAppMapContext, createMapSummary, discoverAppMap, persistAppMapContext, runMappedCommand } from './appMap.js';
7
8
  import { DEFAULT_STARTUP_TIMEOUT_SECONDS, startManagedAppium, statusManagedAppium, stopManagedAppium } from './appiumLifecycle.js';
8
9
  import { runScenario } from './runner.js';
9
10
  import { ensureDir, errorMessage, resolveExecutable, sleep } from './utils.js';
@@ -19,6 +20,22 @@ export class DaemonRequestTimeoutError extends Error {
19
20
  this.name = 'DaemonRequestTimeoutError';
20
21
  }
21
22
  }
23
+ export class DaemonOperationError extends Error {
24
+ data;
25
+ constructor(message, data) {
26
+ super(message);
27
+ this.name = 'DaemonOperationError';
28
+ this.data = data;
29
+ }
30
+ }
31
+ class DaemonResponseError extends Error {
32
+ data;
33
+ constructor(message, data) {
34
+ super(message);
35
+ this.name = 'DaemonResponseError';
36
+ this.data = data;
37
+ }
38
+ }
22
39
  function daemonDir() {
23
40
  return ensureDir(path.join(process.cwd(), '.visor', 'daemon'));
24
41
  }
@@ -139,7 +156,7 @@ async function requestDaemon(request, timeoutMs = 1000) {
139
156
  async function daemonRequestData(request, timeoutMs = 1000) {
140
157
  const response = await requestDaemon(request, timeoutMs);
141
158
  if (!response.ok) {
142
- throw new Error(response.error);
159
+ throw new DaemonOperationError(response.error, response.data);
143
160
  }
144
161
  return response.data;
145
162
  }
@@ -343,11 +360,14 @@ export async function stopVisorDaemon(serverUrl = DEFAULT_SERVER_URL, force = fa
343
360
  appium: await statusManagedAppium(serverUrl)
344
361
  };
345
362
  }
346
- export async function runDaemonAction(runtime, command, args) {
347
- return daemonRequestData({ type: 'action', runtime, command, args }, 300000);
363
+ export async function runDaemonAction(runtime, command, args, mapOptions) {
364
+ return daemonRequestData({ type: 'action', runtime, command, args, mapOptions }, 300000);
348
365
  }
349
- export async function runDaemonScenario(runtime, scenario, device, timeout, outputDir) {
350
- return daemonRequestData({ type: 'scenario', runtime, scenario, device, timeout, outputDir }, 600000);
366
+ export async function runDaemonScenario(runtime, scenario, device, timeout, outputDir, mapOptions) {
367
+ return daemonRequestData({ type: 'scenario', runtime, scenario, device, timeout, outputDir, mapOptions }, 600000);
368
+ }
369
+ export async function runDaemonDiscover(runtime, mapOptions) {
370
+ return daemonRequestData({ type: 'discover', runtime, mapOptions }, 300000);
351
371
  }
352
372
  export async function runDaemonFromEnv() {
353
373
  const options = {
@@ -398,10 +418,10 @@ export async function runDaemonFromEnv() {
398
418
  // The backing WebDriver session is already gone; removing it from the cache is enough.
399
419
  }
400
420
  }
401
- async function runActionWithSessionRetry(runtime, command, args) {
421
+ async function runActionWithSessionRetry(runtime, command, args, mapOptions) {
402
422
  const adapter = await sessionFor(runtime);
403
423
  try {
404
- return await adapter[command](args);
424
+ return await runActionWithMap(adapter, command, args, mapOptions);
405
425
  }
406
426
  catch (error) {
407
427
  if (!isRecoverableSessionCacheError(error)) {
@@ -409,7 +429,46 @@ export async function runDaemonFromEnv() {
409
429
  }
410
430
  await discardSession(runtime);
411
431
  const freshAdapter = await sessionFor(runtime);
412
- return freshAdapter[command](args);
432
+ return runActionWithMap(freshAdapter, command, args, mapOptions);
433
+ }
434
+ }
435
+ async function runActionWithMap(adapter, command, args, mapOptions) {
436
+ const options = mapOptions;
437
+ const context = createAppMapContext(adapter, options);
438
+ if (!context) {
439
+ const summary = createMapSummary(adapter, options);
440
+ try {
441
+ const details = await adapter[command](args);
442
+ return {
443
+ ...details,
444
+ map: summary
445
+ };
446
+ }
447
+ catch (error) {
448
+ throw new DaemonResponseError(errorMessage(error), {
449
+ map: summary
450
+ });
451
+ }
452
+ }
453
+ try {
454
+ const details = await runMappedCommand(context, command, args);
455
+ const stepMap = details.map && typeof details.map === 'object' ? details.map : {};
456
+ const summary = persistAppMapContext(context);
457
+ return {
458
+ ...details,
459
+ map: {
460
+ ...summary,
461
+ ...stepMap
462
+ }
463
+ };
464
+ }
465
+ catch (error) {
466
+ throw new DaemonResponseError(errorMessage(error), {
467
+ map: persistAppMapContext(context)
468
+ });
469
+ }
470
+ finally {
471
+ persistAppMapContext(context);
413
472
  }
414
473
  }
415
474
  function enqueue(work) {
@@ -463,11 +522,24 @@ export async function runDaemonFromEnv() {
463
522
  });
464
523
  return;
465
524
  }
525
+ else if (request.type === 'discover') {
526
+ const data = await enqueue(async () => {
527
+ activeOperation = `discover:${runtimeKey(request.runtime)}`;
528
+ try {
529
+ const adapter = await sessionFor(request.runtime);
530
+ return await discoverAppMap(adapter, request.mapOptions);
531
+ }
532
+ finally {
533
+ activeOperation = null;
534
+ }
535
+ });
536
+ response = { ok: true, data };
537
+ }
466
538
  else if (request.type === 'action') {
467
539
  const data = await enqueue(async () => {
468
540
  activeOperation = `${request.command}:${runtimeKey(request.runtime)}`;
469
541
  try {
470
- return await runActionWithSessionRetry(request.runtime, request.command, request.args);
542
+ return await runActionWithSessionRetry(request.runtime, request.command, request.args, request.mapOptions);
471
543
  }
472
544
  finally {
473
545
  activeOperation = null;
@@ -480,7 +552,7 @@ export async function runDaemonFromEnv() {
480
552
  activeOperation = `scenario:${runtimeKey(request.runtime)}`;
481
553
  try {
482
554
  const adapter = await sessionFor(request.runtime);
483
- return await runScenario(request.scenario, adapter, request.device, request.timeout, request.outputDir, false);
555
+ return await runScenario(request.scenario, adapter, request.device, request.timeout, request.outputDir, false, request.mapOptions);
484
556
  }
485
557
  finally {
486
558
  activeOperation = null;
@@ -490,7 +562,11 @@ export async function runDaemonFromEnv() {
490
562
  }
491
563
  }
492
564
  catch (error) {
493
- response = { ok: false, error: errorMessage(error) };
565
+ response = {
566
+ ok: false,
567
+ error: errorMessage(error),
568
+ data: error instanceof DaemonResponseError ? error.data : undefined
569
+ };
494
570
  }
495
571
  socket.end(`${JSON.stringify(response)}\n`);
496
572
  })();
@@ -0,0 +1,116 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const TINY_PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMB/axDRf0AAAAASUVORK5CYII=', 'base64');
4
+ function resolveArtifactPath(args, extension, fallbackLabel) {
5
+ const label = String(args.label ?? fallbackLabel);
6
+ return path.resolve(typeof args.path === 'string' ? args.path : `${label}.${extension}`);
7
+ }
8
+ function targetValue(target) {
9
+ const separatorIndex = target.indexOf('=');
10
+ return separatorIndex === -1 ? target : target.slice(separatorIndex + 1);
11
+ }
12
+ export class LocalRuntimeAdapter {
13
+ counter = 0;
14
+ route = 'app://home';
15
+ capability() {
16
+ return {
17
+ platform: 'android',
18
+ commands: ['navigate', 'tap', 'act', 'scroll', 'screenshot', 'wait', 'source']
19
+ };
20
+ }
21
+ async navigate(args) {
22
+ this.route = String(args.to ?? this.route);
23
+ return {
24
+ action: 'navigate',
25
+ platform: 'android',
26
+ args: { to: this.route }
27
+ };
28
+ }
29
+ async tap(args) {
30
+ const target = String(args.target ?? '');
31
+ if (targetValue(target) === 'Increment') {
32
+ this.counter += 1;
33
+ }
34
+ return {
35
+ action: 'tap',
36
+ platform: 'android',
37
+ args: { target }
38
+ };
39
+ }
40
+ async act(args) {
41
+ return {
42
+ action: 'act',
43
+ platform: 'android',
44
+ args
45
+ };
46
+ }
47
+ async scroll(args) {
48
+ return {
49
+ action: 'scroll',
50
+ platform: 'android',
51
+ args: {
52
+ direction: args.direction ?? 'down',
53
+ percent: args.percent ?? 70
54
+ }
55
+ };
56
+ }
57
+ async screenshot(args) {
58
+ const label = String(args.label ?? 'capture');
59
+ const filePath = resolveArtifactPath(args, 'png', label);
60
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
61
+ fs.writeFileSync(filePath, TINY_PNG);
62
+ return {
63
+ action: 'screenshot',
64
+ platform: 'android',
65
+ args: {
66
+ label,
67
+ file: path.basename(filePath),
68
+ path: filePath,
69
+ width: 1,
70
+ height: 1
71
+ }
72
+ };
73
+ }
74
+ async wait(args) {
75
+ return {
76
+ action: 'wait',
77
+ platform: 'android',
78
+ args: { ms: Number(args.ms ?? 0) }
79
+ };
80
+ }
81
+ async source(args) {
82
+ const label = String(args.label ?? 'source');
83
+ const filePath = resolveArtifactPath(args, 'xml', label);
84
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
85
+ const content = this.sourceXml();
86
+ fs.writeFileSync(filePath, content, 'utf8');
87
+ return {
88
+ action: 'source',
89
+ platform: 'android',
90
+ args: {
91
+ label,
92
+ file: path.basename(filePath),
93
+ path: filePath,
94
+ format: 'xml',
95
+ bytes: Buffer.byteLength(content, 'utf8')
96
+ }
97
+ };
98
+ }
99
+ async exists(target) {
100
+ const value = targetValue(target);
101
+ return value === 'Increment' || value === String(this.counter) || this.sourceXml().includes(value);
102
+ }
103
+ async close() {
104
+ return undefined;
105
+ }
106
+ sourceXml() {
107
+ return [
108
+ '<hierarchy>',
109
+ ` <node text="Route: ${this.route}" />`,
110
+ ` <node text="${this.counter}" label="${this.counter}" name="${this.counter}" value="${this.counter}" />`,
111
+ ' <node text="Increment" content-desc="Increment" label="Increment" name="Increment" />',
112
+ '</hierarchy>',
113
+ ''
114
+ ].join('\n');
115
+ }
116
+ }
package/dist/main.js CHANGED
@@ -12,6 +12,12 @@ function isHelpData(value) {
12
12
  'usageText' in value &&
13
13
  typeof value.usageText === 'string');
14
14
  }
15
+ function isVersionData(value) {
16
+ return Boolean(value &&
17
+ typeof value === 'object' &&
18
+ 'versionText' in value &&
19
+ typeof value.versionText === 'string');
20
+ }
15
21
  async function main(argv = process.argv.slice(2)) {
16
22
  try {
17
23
  if (argv[0] === '__daemon') {
@@ -22,6 +28,9 @@ async function main(argv = process.argv.slice(2)) {
22
28
  if (isHelpData(result.response.data)) {
23
29
  console.log(result.response.data.usageText);
24
30
  }
31
+ else if (isVersionData(result.response.data)) {
32
+ console.log(result.response.data.versionText);
33
+ }
25
34
  else {
26
35
  console.log(JSON.stringify(result.response, null, 2));
27
36
  }