@testingbot/cli 1.0.1 → 1.0.3
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 +84 -7
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +55 -8
- package/dist/logger.js +4 -4
- package/dist/models/espresso_options.d.ts +9 -0
- package/dist/models/espresso_options.d.ts.map +1 -1
- package/dist/models/espresso_options.js +14 -0
- package/dist/models/maestro_options.d.ts +20 -7
- package/dist/models/maestro_options.d.ts.map +1 -1
- package/dist/models/maestro_options.js +22 -9
- package/dist/models/testingbot_error.d.ts +3 -0
- package/dist/models/testingbot_error.d.ts.map +1 -1
- package/dist/models/testingbot_error.js +5 -0
- package/dist/models/xcuitest_options.d.ts +9 -0
- package/dist/models/xcuitest_options.d.ts.map +1 -1
- package/dist/models/xcuitest_options.js +14 -0
- package/dist/providers/base_provider.d.ts +119 -0
- package/dist/providers/base_provider.d.ts.map +1 -0
- package/dist/providers/base_provider.js +296 -0
- package/dist/providers/espresso.d.ts +14 -21
- package/dist/providers/espresso.d.ts.map +1 -1
- package/dist/providers/espresso.js +50 -181
- package/dist/providers/login.d.ts +1 -0
- package/dist/providers/login.d.ts.map +1 -1
- package/dist/providers/login.js +16 -7
- package/dist/providers/maestro.d.ts +73 -21
- package/dist/providers/maestro.d.ts.map +1 -1
- package/dist/providers/maestro.js +842 -276
- package/dist/providers/xcuitest.d.ts +14 -21
- package/dist/providers/xcuitest.d.ts.map +1 -1
- package/dist/providers/xcuitest.js +50 -181
- package/dist/upload.d.ts +10 -0
- package/dist/upload.d.ts.map +1 -1
- package/dist/upload.js +46 -21
- package/dist/utils/connectivity.d.ts +26 -0
- package/dist/utils/connectivity.d.ts.map +1 -0
- package/dist/utils/connectivity.js +131 -0
- package/dist/utils/error-helpers.d.ts +26 -0
- package/dist/utils/error-helpers.d.ts.map +1 -0
- package/dist/utils/error-helpers.js +237 -0
- package/dist/utils/file-type-detector.d.ts +4 -1
- package/dist/utils/file-type-detector.d.ts.map +1 -1
- package/dist/utils/file-type-detector.js +30 -6
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +8 -3
- package/package.json +2 -2
|
@@ -10,84 +10,48 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
10
10
|
const socket_io_client_1 = require("socket.io-client");
|
|
11
11
|
const testingbot_error_1 = __importDefault(require("../models/testingbot_error"));
|
|
12
12
|
const utils_1 = __importDefault(require("../utils"));
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
class Espresso {
|
|
13
|
+
const base_provider_1 = __importDefault(require("./base_provider"));
|
|
14
|
+
class Espresso extends base_provider_1.default {
|
|
16
15
|
URL = 'https://api.testingbot.com/v1/app-automate/espresso';
|
|
17
|
-
POLL_INTERVAL_MS = 5000;
|
|
18
|
-
MAX_POLL_ATTEMPTS = 720; // 1 hour max with 5s interval
|
|
19
|
-
credentials;
|
|
20
|
-
options;
|
|
21
|
-
upload;
|
|
22
|
-
appId = undefined;
|
|
23
|
-
activeRunIds = [];
|
|
24
|
-
isShuttingDown = false;
|
|
25
|
-
signalHandler = null;
|
|
26
16
|
socket = null;
|
|
27
17
|
updateServer = null;
|
|
28
18
|
updateKey = null;
|
|
29
19
|
constructor(credentials, options) {
|
|
30
|
-
|
|
31
|
-
this.options = options;
|
|
32
|
-
this.upload = new upload_1.default();
|
|
20
|
+
super(credentials, options);
|
|
33
21
|
}
|
|
34
22
|
async validate() {
|
|
35
23
|
if (this.options.app === undefined) {
|
|
36
24
|
throw new testingbot_error_1.default(`app option is required`);
|
|
37
25
|
}
|
|
38
|
-
try {
|
|
39
|
-
await node_fs_1.default.promises.access(this.options.app, node_fs_1.default.constants.R_OK);
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
throw new testingbot_error_1.default(`Provided app path does not exist ${this.options.app}`);
|
|
43
|
-
}
|
|
44
26
|
if (this.options.testApp === undefined) {
|
|
45
27
|
throw new testingbot_error_1.default(`testApp option is required`);
|
|
46
28
|
}
|
|
47
|
-
try {
|
|
48
|
-
await node_fs_1.default.promises.access(this.options.testApp, node_fs_1.default.constants.R_OK);
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
throw new testingbot_error_1.default(`testApp path does not exist ${this.options.testApp}`);
|
|
52
|
-
}
|
|
53
29
|
// Validate report options
|
|
54
30
|
if (this.options.report && !this.options.reportOutputDir) {
|
|
55
31
|
throw new testingbot_error_1.default(`--report-output-dir is required when --report is specified`);
|
|
56
32
|
}
|
|
33
|
+
// Validate file access in parallel for better performance
|
|
34
|
+
const fileChecks = [
|
|
35
|
+
node_fs_1.default.promises.access(this.options.app, node_fs_1.default.constants.R_OK).catch(() => {
|
|
36
|
+
throw new testingbot_error_1.default(`Provided app path does not exist ${this.options.app}`);
|
|
37
|
+
}),
|
|
38
|
+
node_fs_1.default.promises.access(this.options.testApp, node_fs_1.default.constants.R_OK).catch(() => {
|
|
39
|
+
throw new testingbot_error_1.default(`testApp path does not exist ${this.options.testApp}`);
|
|
40
|
+
}),
|
|
41
|
+
];
|
|
57
42
|
if (this.options.reportOutputDir) {
|
|
58
|
-
|
|
43
|
+
fileChecks.push(this.ensureOutputDirectory(this.options.reportOutputDir));
|
|
59
44
|
}
|
|
45
|
+
await Promise.all(fileChecks);
|
|
60
46
|
return true;
|
|
61
47
|
}
|
|
62
|
-
async ensureOutputDirectory(dirPath) {
|
|
63
|
-
try {
|
|
64
|
-
const stat = await node_fs_1.default.promises.stat(dirPath);
|
|
65
|
-
if (!stat.isDirectory()) {
|
|
66
|
-
throw new testingbot_error_1.default(`Report output path exists but is not a directory: ${dirPath}`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
catch (error) {
|
|
70
|
-
if (error.code === 'ENOENT') {
|
|
71
|
-
try {
|
|
72
|
-
await node_fs_1.default.promises.mkdir(dirPath, { recursive: true });
|
|
73
|
-
}
|
|
74
|
-
catch (mkdirError) {
|
|
75
|
-
throw new testingbot_error_1.default(`Failed to create report output directory: ${dirPath}`, { cause: mkdirError });
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
else if (error instanceof testingbot_error_1.default) {
|
|
79
|
-
throw error;
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
throw new testingbot_error_1.default(`Failed to access report output directory: ${dirPath}`, { cause: error });
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
48
|
async run() {
|
|
87
49
|
if (!(await this.validate())) {
|
|
88
50
|
return { success: false, runs: [] };
|
|
89
51
|
}
|
|
90
52
|
try {
|
|
53
|
+
// Quick connectivity check before starting uploads
|
|
54
|
+
await this.ensureConnectivity();
|
|
91
55
|
if (!this.options.quiet) {
|
|
92
56
|
logger_1.default.info('Uploading Espresso App');
|
|
93
57
|
}
|
|
@@ -158,9 +122,11 @@ class Espresso {
|
|
|
158
122
|
try {
|
|
159
123
|
const capabilities = this.options.getCapabilities();
|
|
160
124
|
const espressoOptions = this.options.getEspressoOptions();
|
|
125
|
+
const metadata = this.options.metadata;
|
|
161
126
|
const response = await axios_1.default.post(`${this.URL}/${this.appId}/run`, {
|
|
162
127
|
capabilities: [capabilities],
|
|
163
128
|
...(espressoOptions && { espressoOptions }),
|
|
129
|
+
...(metadata && { metadata }),
|
|
164
130
|
}, {
|
|
165
131
|
headers: {
|
|
166
132
|
'Content-Type': 'application/json',
|
|
@@ -170,6 +136,7 @@ class Espresso {
|
|
|
170
136
|
username: this.credentials.userName,
|
|
171
137
|
password: this.credentials.accessKey,
|
|
172
138
|
},
|
|
139
|
+
timeout: 30000, // 30 second timeout
|
|
173
140
|
});
|
|
174
141
|
// Check for version update notification
|
|
175
142
|
const latestVersion = response.headers?.['x-testingbotctl-version'];
|
|
@@ -192,28 +159,30 @@ class Espresso {
|
|
|
192
159
|
if (error instanceof testingbot_error_1.default) {
|
|
193
160
|
throw error;
|
|
194
161
|
}
|
|
195
|
-
throw
|
|
196
|
-
cause: error,
|
|
197
|
-
});
|
|
162
|
+
throw await this.handleErrorWithDiagnostics(error, 'Running Espresso test failed');
|
|
198
163
|
}
|
|
199
164
|
}
|
|
200
165
|
async getStatus() {
|
|
201
166
|
try {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
167
|
+
return await this.withRetry('Getting Espresso test status', async () => {
|
|
168
|
+
const response = await axios_1.default.get(`${this.URL}/${this.appId}`, {
|
|
169
|
+
headers: {
|
|
170
|
+
'User-Agent': utils_1.default.getUserAgent(),
|
|
171
|
+
},
|
|
172
|
+
auth: {
|
|
173
|
+
username: this.credentials.userName,
|
|
174
|
+
password: this.credentials.accessKey,
|
|
175
|
+
},
|
|
176
|
+
timeout: 30000, // 30 second timeout
|
|
177
|
+
});
|
|
178
|
+
// Check for version update notification
|
|
179
|
+
const latestVersion = response.headers?.['x-testingbotctl-version'];
|
|
180
|
+
utils_1.default.checkForUpdate(latestVersion);
|
|
181
|
+
return response.data;
|
|
210
182
|
});
|
|
211
|
-
return response.data;
|
|
212
183
|
}
|
|
213
184
|
catch (error) {
|
|
214
|
-
throw
|
|
215
|
-
cause: error,
|
|
216
|
-
});
|
|
185
|
+
throw await this.handleErrorWithDiagnostics(error, 'Failed to get Espresso test status');
|
|
217
186
|
}
|
|
218
187
|
}
|
|
219
188
|
async waitForCompletion() {
|
|
@@ -240,7 +209,7 @@ class Espresso {
|
|
|
240
209
|
for (const run of status.runs) {
|
|
241
210
|
const statusEmoji = run.success === 1 ? '✅' : '❌';
|
|
242
211
|
const statusText = run.success === 1 ? 'Test completed successfully' : 'Test failed';
|
|
243
|
-
console.log(` ${statusEmoji} Run ${run.id} (${run
|
|
212
|
+
console.log(` ${statusEmoji} Run ${run.id} (${this.getRunDisplayName(run)}): ${statusText}`);
|
|
244
213
|
}
|
|
245
214
|
}
|
|
246
215
|
const allSucceeded = status.runs.every((run) => run.success === 1);
|
|
@@ -253,7 +222,7 @@ class Espresso {
|
|
|
253
222
|
const failedRuns = status.runs.filter((run) => run.success !== 1);
|
|
254
223
|
logger_1.default.error(`${failedRuns.length} test run(s) failed:`);
|
|
255
224
|
for (const run of failedRuns) {
|
|
256
|
-
logger_1.default.error(` - Run ${run.id} (${run
|
|
225
|
+
logger_1.default.error(` - Run ${run.id} (${this.getRunDisplayName(run)}): ${run.report || 'No report available'}`);
|
|
257
226
|
}
|
|
258
227
|
}
|
|
259
228
|
// Fetch reports if requested
|
|
@@ -284,24 +253,20 @@ class Espresso {
|
|
|
284
253
|
previousStatus.set(run.id, run.status);
|
|
285
254
|
const statusInfo = this.getStatusInfo(run.status);
|
|
286
255
|
if (run.status === 'WAITING' || run.status === 'READY') {
|
|
287
|
-
const message = ` ${statusInfo.emoji} Run ${run.id} (${run
|
|
256
|
+
const message = ` ${statusInfo.emoji} Run ${run.id} (${this.getRunDisplayName(run)}): ${statusInfo.text} (${elapsedStr})`;
|
|
288
257
|
process.stdout.write(`\r${message}`);
|
|
289
258
|
}
|
|
290
259
|
else if (statusChanged) {
|
|
291
|
-
console.log(` ${statusInfo.emoji} Run ${run.id} (${run
|
|
260
|
+
console.log(` ${statusInfo.emoji} Run ${run.id} (${this.getRunDisplayName(run)}): ${statusInfo.text}`);
|
|
292
261
|
}
|
|
293
262
|
}
|
|
294
263
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
const minutes = Math.floor(seconds / 60);
|
|
303
|
-
const remainingSeconds = seconds % 60;
|
|
304
|
-
return `${minutes}m ${remainingSeconds}s`;
|
|
264
|
+
/**
|
|
265
|
+
* Get the display name for a run, preferring environment.name over capabilities.deviceName
|
|
266
|
+
* This shows the actual device used when a wildcard (*) was specified
|
|
267
|
+
*/
|
|
268
|
+
getRunDisplayName(run) {
|
|
269
|
+
return run.environment?.name || run.capabilities.deviceName;
|
|
305
270
|
}
|
|
306
271
|
getStatusInfo(status) {
|
|
307
272
|
switch (status) {
|
|
@@ -340,7 +305,11 @@ class Espresso {
|
|
|
340
305
|
username: this.credentials.userName,
|
|
341
306
|
password: this.credentials.accessKey,
|
|
342
307
|
},
|
|
308
|
+
timeout: 30000, // 30 second timeout
|
|
343
309
|
});
|
|
310
|
+
// Check for version update notification
|
|
311
|
+
const latestVersion = response.headers?.['x-testingbotctl-version'];
|
|
312
|
+
utils_1.default.checkForUpdate(latestVersion);
|
|
344
313
|
const reportContent = response.data;
|
|
345
314
|
if (!reportContent) {
|
|
346
315
|
logger_1.default.error('No report content received');
|
|
@@ -357,106 +326,6 @@ class Espresso {
|
|
|
357
326
|
logger_1.default.error(`Failed to fetch report: ${error instanceof Error ? error.message : error}`);
|
|
358
327
|
}
|
|
359
328
|
}
|
|
360
|
-
sleep(ms) {
|
|
361
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
362
|
-
}
|
|
363
|
-
extractErrorMessage(cause) {
|
|
364
|
-
if (typeof cause === 'string') {
|
|
365
|
-
return cause;
|
|
366
|
-
}
|
|
367
|
-
if (Array.isArray(cause)) {
|
|
368
|
-
return cause.join('\n');
|
|
369
|
-
}
|
|
370
|
-
if (cause && typeof cause === 'object') {
|
|
371
|
-
const axiosError = cause;
|
|
372
|
-
if (axiosError.response?.data?.errors) {
|
|
373
|
-
return axiosError.response.data.errors.join('\n');
|
|
374
|
-
}
|
|
375
|
-
if (axiosError.response?.data?.error) {
|
|
376
|
-
return axiosError.response.data.error;
|
|
377
|
-
}
|
|
378
|
-
if (axiosError.response?.data?.message) {
|
|
379
|
-
return axiosError.response.data.message;
|
|
380
|
-
}
|
|
381
|
-
if (cause instanceof Error) {
|
|
382
|
-
return cause.message;
|
|
383
|
-
}
|
|
384
|
-
const obj = cause;
|
|
385
|
-
if (obj.errors) {
|
|
386
|
-
return obj.errors.join('\n');
|
|
387
|
-
}
|
|
388
|
-
if (obj.error) {
|
|
389
|
-
return obj.error;
|
|
390
|
-
}
|
|
391
|
-
if (obj.message) {
|
|
392
|
-
return obj.message;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
return null;
|
|
396
|
-
}
|
|
397
|
-
setupSignalHandlers() {
|
|
398
|
-
this.signalHandler = () => {
|
|
399
|
-
this.handleShutdown();
|
|
400
|
-
};
|
|
401
|
-
platform_1.default.setupSignalHandlers(this.signalHandler);
|
|
402
|
-
}
|
|
403
|
-
removeSignalHandlers() {
|
|
404
|
-
if (this.signalHandler) {
|
|
405
|
-
platform_1.default.removeSignalHandlers(this.signalHandler);
|
|
406
|
-
this.signalHandler = null;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
handleShutdown() {
|
|
410
|
-
if (this.isShuttingDown) {
|
|
411
|
-
logger_1.default.warn('Force exiting...');
|
|
412
|
-
process.exit(1);
|
|
413
|
-
}
|
|
414
|
-
this.isShuttingDown = true;
|
|
415
|
-
this.clearLine();
|
|
416
|
-
logger_1.default.warn('Received interrupt signal, stopping test runs...');
|
|
417
|
-
this.stopActiveRuns()
|
|
418
|
-
.then(() => {
|
|
419
|
-
logger_1.default.info('All test runs have been stopped.');
|
|
420
|
-
process.exit(1);
|
|
421
|
-
})
|
|
422
|
-
.catch((error) => {
|
|
423
|
-
logger_1.default.error(`Failed to stop some test runs: ${error instanceof Error ? error.message : error}`);
|
|
424
|
-
process.exit(1);
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
async stopActiveRuns() {
|
|
428
|
-
if (!this.appId || this.activeRunIds.length === 0) {
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
const stopPromises = this.activeRunIds.map((runId) => this.stopRun(runId).catch((error) => {
|
|
432
|
-
logger_1.default.error(`Failed to stop run ${runId}: ${error instanceof Error ? error.message : error}`);
|
|
433
|
-
}));
|
|
434
|
-
await Promise.all(stopPromises);
|
|
435
|
-
}
|
|
436
|
-
async stopRun(runId) {
|
|
437
|
-
if (!this.appId) {
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
try {
|
|
441
|
-
await axios_1.default.post(`${this.URL}/${this.appId}/${runId}/stop`, {}, {
|
|
442
|
-
headers: {
|
|
443
|
-
'User-Agent': utils_1.default.getUserAgent(),
|
|
444
|
-
},
|
|
445
|
-
auth: {
|
|
446
|
-
username: this.credentials.userName,
|
|
447
|
-
password: this.credentials.accessKey,
|
|
448
|
-
},
|
|
449
|
-
});
|
|
450
|
-
if (!this.options.quiet) {
|
|
451
|
-
logger_1.default.info(` Stopped run ${runId}`);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
catch (error) {
|
|
455
|
-
throw new testingbot_error_1.default(`Failed to stop run ${runId}`, {
|
|
456
|
-
cause: error,
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
329
|
connectToUpdateServer() {
|
|
461
330
|
if (!this.updateServer || !this.updateKey || this.options.quiet) {
|
|
462
331
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/providers/login.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/providers/login.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,CAAC,OAAO,OAAO,KAAK;IACxB,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,IAAI,CAAa;IAEZ,GAAG,IAAI,OAAO,CAAC,WAAW,CAAC;IAiCxC,OAAO,CAAC,WAAW;IAoBnB,OAAO,CAAC,UAAU;IAOlB,OAAO,CAAC,eAAe;IA4DvB,OAAO,CAAC,gBAAgB;IAoCxB,OAAO,CAAC,mBAAmB;IAoD3B,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,iBAAiB;YAyDX,eAAe;YAKf,WAAW;CAiB1B"}
|
package/dist/providers/login.js
CHANGED
|
@@ -8,7 +8,10 @@ const node_url_1 = require("node:url");
|
|
|
8
8
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
10
|
const node_os_1 = __importDefault(require("node:os"));
|
|
11
|
+
const node_child_process_1 = require("node:child_process");
|
|
12
|
+
const node_util_1 = require("node:util");
|
|
11
13
|
const logger_1 = __importDefault(require("../logger"));
|
|
14
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
12
15
|
const AUTH_URL = 'https://testingbot.com/auth';
|
|
13
16
|
class Login {
|
|
14
17
|
server = null;
|
|
@@ -200,7 +203,16 @@ class Login {
|
|
|
200
203
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
201
204
|
res.end(html);
|
|
202
205
|
}
|
|
206
|
+
escapeHtml(text) {
|
|
207
|
+
return text
|
|
208
|
+
.replace(/&/g, '&')
|
|
209
|
+
.replace(/</g, '<')
|
|
210
|
+
.replace(/>/g, '>')
|
|
211
|
+
.replace(/"/g, '"')
|
|
212
|
+
.replace(/'/g, ''');
|
|
213
|
+
}
|
|
203
214
|
sendErrorResponse(res, error) {
|
|
215
|
+
const safeError = this.escapeHtml(error);
|
|
204
216
|
const html = `<!DOCTYPE html>
|
|
205
217
|
<html>
|
|
206
218
|
<head>
|
|
@@ -246,7 +258,7 @@ class Login {
|
|
|
246
258
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
247
259
|
</svg>
|
|
248
260
|
<h1>Authentication Failed</h1>
|
|
249
|
-
<p class="error">${
|
|
261
|
+
<p class="error">${safeError}</p>
|
|
250
262
|
<p>Please try again or contact support.</p>
|
|
251
263
|
</div>
|
|
252
264
|
</body>
|
|
@@ -259,20 +271,17 @@ class Login {
|
|
|
259
271
|
await node_fs_1.default.promises.writeFile(filePath, `${key}:${secret}`, { mode: 0o600 });
|
|
260
272
|
}
|
|
261
273
|
async openBrowser(url) {
|
|
262
|
-
const { exec } = await import('node:child_process');
|
|
263
|
-
const { promisify } = await import('node:util');
|
|
264
|
-
const execAsync = promisify(exec);
|
|
265
274
|
const platform = process.platform;
|
|
266
275
|
try {
|
|
267
276
|
if (platform === 'darwin') {
|
|
268
|
-
await
|
|
277
|
+
await execFileAsync('open', [url]);
|
|
269
278
|
}
|
|
270
279
|
else if (platform === 'win32') {
|
|
271
|
-
await
|
|
280
|
+
await execFileAsync('cmd', ['/c', 'start', '', url]);
|
|
272
281
|
}
|
|
273
282
|
else {
|
|
274
283
|
// Linux and others
|
|
275
|
-
await
|
|
284
|
+
await execFileAsync('xdg-open', [url]);
|
|
276
285
|
}
|
|
277
286
|
}
|
|
278
287
|
catch {
|
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
import MaestroOptions from '../models/maestro_options';
|
|
2
2
|
import Credentials from '../models/credentials';
|
|
3
|
+
import BaseProvider from './base_provider';
|
|
3
4
|
export interface MaestroRunAssets {
|
|
4
5
|
logs?: Record<string, string>;
|
|
5
6
|
video?: string | false;
|
|
6
7
|
screenshots?: string[];
|
|
7
8
|
}
|
|
9
|
+
export type MaestroFlowStatus = 'WAITING' | 'READY' | 'DONE' | 'FAILED';
|
|
10
|
+
export interface MaestroFlowInfo {
|
|
11
|
+
id: number;
|
|
12
|
+
name: string;
|
|
13
|
+
report?: string;
|
|
14
|
+
requested_at?: string;
|
|
15
|
+
completed_at?: string;
|
|
16
|
+
status: MaestroFlowStatus;
|
|
17
|
+
success?: number;
|
|
18
|
+
test_case_id?: number;
|
|
19
|
+
error_messages?: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface MaestroRunEnvironment {
|
|
22
|
+
device?: string;
|
|
23
|
+
name?: string;
|
|
24
|
+
version?: string;
|
|
25
|
+
}
|
|
8
26
|
export interface MaestroRunInfo {
|
|
9
27
|
id: number;
|
|
10
28
|
status: 'WAITING' | 'READY' | 'DONE' | 'FAILED';
|
|
@@ -13,10 +31,13 @@ export interface MaestroRunInfo {
|
|
|
13
31
|
platformName: string;
|
|
14
32
|
version?: string;
|
|
15
33
|
};
|
|
34
|
+
environment?: MaestroRunEnvironment;
|
|
16
35
|
success: number;
|
|
17
36
|
report?: string;
|
|
18
37
|
options?: Record<string, unknown>;
|
|
19
38
|
assets?: MaestroRunAssets;
|
|
39
|
+
flows?: MaestroFlowInfo[];
|
|
40
|
+
error_messages?: string[];
|
|
20
41
|
}
|
|
21
42
|
export interface MaestroRunDetails extends MaestroRunInfo {
|
|
22
43
|
completed: boolean;
|
|
@@ -35,41 +56,79 @@ export interface MaestroSocketMessage {
|
|
|
35
56
|
id: number;
|
|
36
57
|
payload: string;
|
|
37
58
|
}
|
|
38
|
-
export
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
private appId;
|
|
59
|
+
export interface MissingFileReference {
|
|
60
|
+
flowFile: string;
|
|
61
|
+
referencedFile: string;
|
|
62
|
+
resolvedPath: string;
|
|
63
|
+
}
|
|
64
|
+
export default class Maestro extends BaseProvider<MaestroOptions> {
|
|
65
|
+
protected readonly URL = "https://api.testingbot.com/v1/app-automate/maestro";
|
|
46
66
|
private detectedPlatform;
|
|
47
|
-
private activeRunIds;
|
|
48
|
-
private isShuttingDown;
|
|
49
|
-
private signalHandler;
|
|
50
67
|
private socket;
|
|
51
68
|
private updateServer;
|
|
52
69
|
private updateKey;
|
|
53
70
|
constructor(credentials: Credentials, options: MaestroOptions);
|
|
71
|
+
private static readonly SUPPORTED_APP_EXTENSIONS;
|
|
54
72
|
private validate;
|
|
55
|
-
private ensureOutputDirectory;
|
|
56
73
|
/**
|
|
57
74
|
* Detect platform from app file content using magic bytes
|
|
58
75
|
*/
|
|
59
76
|
private detectPlatform;
|
|
60
77
|
run(): Promise<MaestroResult>;
|
|
61
78
|
private uploadApp;
|
|
79
|
+
private checkAppChecksum;
|
|
62
80
|
private uploadFlows;
|
|
63
81
|
private discoverFlows;
|
|
64
82
|
private discoverDependencies;
|
|
83
|
+
/**
|
|
84
|
+
* Check if a string looks like a file path (relative path with extension)
|
|
85
|
+
*/
|
|
86
|
+
private looksLikePath;
|
|
87
|
+
/**
|
|
88
|
+
* Try to add a file path as a dependency if it exists
|
|
89
|
+
*/
|
|
90
|
+
private tryAddDependency;
|
|
91
|
+
/**
|
|
92
|
+
* Recursively extract file paths from any value in the YAML structure
|
|
93
|
+
*/
|
|
94
|
+
private extractPathsFromValue;
|
|
95
|
+
/**
|
|
96
|
+
* Find all file references in flow files that don't exist on disk.
|
|
97
|
+
* This validates that all referenced files (runScript, runFlow, addMedia, etc.)
|
|
98
|
+
* will be included in the zip.
|
|
99
|
+
*/
|
|
100
|
+
findMissingReferences(flowFiles: string[], allIncludedFiles: string[], baseDir?: string): Promise<MissingFileReference[]>;
|
|
101
|
+
/**
|
|
102
|
+
* Recursively find missing file references in a YAML value
|
|
103
|
+
*/
|
|
104
|
+
private findMissingInValue;
|
|
105
|
+
/**
|
|
106
|
+
* Log warnings for missing file references
|
|
107
|
+
*/
|
|
108
|
+
private logMissingReferences;
|
|
109
|
+
private logIncludedFiles;
|
|
65
110
|
private createFlowsZip;
|
|
66
111
|
private runTests;
|
|
67
112
|
private getStatus;
|
|
68
113
|
private waitForCompletion;
|
|
69
114
|
private displayRunStatus;
|
|
70
|
-
|
|
71
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Get the display name for a run, preferring environment.name over capabilities.deviceName
|
|
117
|
+
* This shows the actual device used when a wildcard (*) was specified
|
|
118
|
+
*/
|
|
119
|
+
private getRunDisplayName;
|
|
72
120
|
private getStatusInfo;
|
|
121
|
+
private getFlowStatusDisplay;
|
|
122
|
+
private hasAnyFlowFailed;
|
|
123
|
+
private calculateFlowDuration;
|
|
124
|
+
private getTerminalHeight;
|
|
125
|
+
private getMaxDisplayableFlows;
|
|
126
|
+
private getRemainingSummary;
|
|
127
|
+
private displayFlowsWithLimit;
|
|
128
|
+
private displayFlowsTableHeader;
|
|
129
|
+
private displayFlowRow;
|
|
130
|
+
private displayFlowsTable;
|
|
131
|
+
private updateFlowsInPlace;
|
|
73
132
|
private fetchReports;
|
|
74
133
|
private getRunDetails;
|
|
75
134
|
private waitForArtifactsSync;
|
|
@@ -77,13 +136,6 @@ export default class Maestro {
|
|
|
77
136
|
private generateArtifactZipName;
|
|
78
137
|
private downloadArtifacts;
|
|
79
138
|
private createZipFromDirectory;
|
|
80
|
-
private sleep;
|
|
81
|
-
private extractErrorMessage;
|
|
82
|
-
private setupSignalHandlers;
|
|
83
|
-
private removeSignalHandlers;
|
|
84
|
-
private handleShutdown;
|
|
85
|
-
private stopActiveRuns;
|
|
86
|
-
private stopRun;
|
|
87
139
|
private connectToUpdateServer;
|
|
88
140
|
private disconnectFromUpdateServer;
|
|
89
141
|
private handleMaestroData;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"maestro.d.ts","sourceRoot":"","sources":["../../src/providers/maestro.ts"],"names":[],"mappings":"AAAA,OAAO,cAAiC,MAAM,2BAA2B,CAAC;AAE1E,OAAO,WAAW,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"maestro.d.ts","sourceRoot":"","sources":["../../src/providers/maestro.ts"],"names":[],"mappings":"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"}
|