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/adapters.js +18 -8
- package/dist/appMap.js +721 -0
- package/dist/cli.js +158 -13
- package/dist/daemon.js +87 -11
- package/dist/localRuntime.js +116 -0
- package/dist/main.js +9 -0
- package/dist/runner.js +25 -2
- package/package.json +4 -1
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
|
-
'
|
|
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
|
-
'
|
|
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
|
|
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
|
|
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
|
|
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 = {
|
|
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
|
}
|