@testingbot/cli 1.0.0
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/LICENSE +21 -0
- package/README.md +375 -0
- package/dist/auth.d.ts +16 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +47 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +329 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/logger.d.ts +4 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +20 -0
- package/dist/models/credentials.d.ts +9 -0
- package/dist/models/credentials.d.ts.map +1 -0
- package/dist/models/credentials.js +20 -0
- package/dist/models/espresso_options.d.ts +116 -0
- package/dist/models/espresso_options.d.ts.map +1 -0
- package/dist/models/espresso_options.js +194 -0
- package/dist/models/maestro_options.d.ts +101 -0
- package/dist/models/maestro_options.d.ts.map +1 -0
- package/dist/models/maestro_options.js +176 -0
- package/dist/models/testingbot_error.d.ts +3 -0
- package/dist/models/testingbot_error.d.ts.map +1 -0
- package/dist/models/testingbot_error.js +5 -0
- package/dist/models/xcuitest_options.d.ts +88 -0
- package/dist/models/xcuitest_options.d.ts.map +1 -0
- package/dist/models/xcuitest_options.js +146 -0
- package/dist/providers/espresso.d.ts +67 -0
- package/dist/providers/espresso.d.ts.map +1 -0
- package/dist/providers/espresso.js +527 -0
- package/dist/providers/login.d.ts +18 -0
- package/dist/providers/login.d.ts.map +1 -0
- package/dist/providers/login.js +284 -0
- package/dist/providers/maestro.d.ts +92 -0
- package/dist/providers/maestro.d.ts.map +1 -0
- package/dist/providers/maestro.js +1010 -0
- package/dist/providers/xcuitest.d.ts +67 -0
- package/dist/providers/xcuitest.d.ts.map +1 -0
- package/dist/providers/xcuitest.js +529 -0
- package/dist/upload.d.ts +21 -0
- package/dist/upload.d.ts.map +1 -0
- package/dist/upload.js +94 -0
- package/dist/utils/file-type-detector.d.ts +15 -0
- package/dist/utils/file-type-detector.d.ts.map +1 -0
- package/dist/utils/file-type-detector.js +38 -0
- package/dist/utils/platform.d.ts +26 -0
- package/dist/utils/platform.d.ts.map +1 -0
- package/dist/utils/platform.js +58 -0
- package/dist/utils.d.ts +15 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +48 -0
- package/package.json +78 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const socket_io_client_1 = require("socket.io-client");
|
|
11
|
+
const testingbot_error_1 = __importDefault(require("../models/testingbot_error"));
|
|
12
|
+
const utils_1 = __importDefault(require("../utils"));
|
|
13
|
+
const upload_1 = __importDefault(require("../upload"));
|
|
14
|
+
const platform_1 = __importDefault(require("../utils/platform"));
|
|
15
|
+
class Espresso {
|
|
16
|
+
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
|
+
socket = null;
|
|
27
|
+
updateServer = null;
|
|
28
|
+
updateKey = null;
|
|
29
|
+
constructor(credentials, options) {
|
|
30
|
+
this.credentials = credentials;
|
|
31
|
+
this.options = options;
|
|
32
|
+
this.upload = new upload_1.default();
|
|
33
|
+
}
|
|
34
|
+
async validate() {
|
|
35
|
+
if (this.options.app === undefined) {
|
|
36
|
+
throw new testingbot_error_1.default(`app option is required`);
|
|
37
|
+
}
|
|
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
|
+
if (this.options.testApp === undefined) {
|
|
45
|
+
throw new testingbot_error_1.default(`testApp option is required`);
|
|
46
|
+
}
|
|
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
|
+
// Validate report options
|
|
54
|
+
if (this.options.report && !this.options.reportOutputDir) {
|
|
55
|
+
throw new testingbot_error_1.default(`--report-output-dir is required when --report is specified`);
|
|
56
|
+
}
|
|
57
|
+
if (this.options.reportOutputDir) {
|
|
58
|
+
await this.ensureOutputDirectory(this.options.reportOutputDir);
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
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
|
+
async run() {
|
|
87
|
+
if (!(await this.validate())) {
|
|
88
|
+
return { success: false, runs: [] };
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
if (!this.options.quiet) {
|
|
92
|
+
logger_1.default.info('Uploading Espresso App');
|
|
93
|
+
}
|
|
94
|
+
await this.uploadApp();
|
|
95
|
+
if (!this.options.quiet) {
|
|
96
|
+
logger_1.default.info('Uploading Espresso Test App');
|
|
97
|
+
}
|
|
98
|
+
await this.uploadTestApp();
|
|
99
|
+
if (!this.options.quiet) {
|
|
100
|
+
logger_1.default.info('Running Espresso Tests');
|
|
101
|
+
}
|
|
102
|
+
await this.runTests();
|
|
103
|
+
if (this.options.async) {
|
|
104
|
+
if (!this.options.quiet) {
|
|
105
|
+
logger_1.default.info(`Tests started in async mode. Project ID: ${this.appId}`);
|
|
106
|
+
}
|
|
107
|
+
return { success: true, runs: [] };
|
|
108
|
+
}
|
|
109
|
+
// Set up signal handlers before waiting for completion
|
|
110
|
+
this.setupSignalHandlers();
|
|
111
|
+
// Connect to real-time update server (unless --quiet is specified)
|
|
112
|
+
this.connectToUpdateServer();
|
|
113
|
+
if (!this.options.quiet) {
|
|
114
|
+
logger_1.default.info('Waiting for test results...');
|
|
115
|
+
}
|
|
116
|
+
const result = await this.waitForCompletion();
|
|
117
|
+
// Clean up
|
|
118
|
+
this.disconnectFromUpdateServer();
|
|
119
|
+
this.removeSignalHandlers();
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
// Clean up on error
|
|
124
|
+
this.disconnectFromUpdateServer();
|
|
125
|
+
this.removeSignalHandlers();
|
|
126
|
+
logger_1.default.error(error instanceof Error ? error.message : error);
|
|
127
|
+
if (error instanceof Error && error.cause) {
|
|
128
|
+
const causeMessage = this.extractErrorMessage(error.cause);
|
|
129
|
+
if (causeMessage) {
|
|
130
|
+
logger_1.default.error(` Reason: ${causeMessage}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { success: false, runs: [] };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async uploadApp() {
|
|
137
|
+
const result = await this.upload.upload({
|
|
138
|
+
filePath: this.options.app,
|
|
139
|
+
url: `${this.URL}/app`,
|
|
140
|
+
credentials: this.credentials,
|
|
141
|
+
contentType: 'application/vnd.android.package-archive',
|
|
142
|
+
showProgress: !this.options.quiet,
|
|
143
|
+
});
|
|
144
|
+
this.appId = result.id;
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
async uploadTestApp() {
|
|
148
|
+
await this.upload.upload({
|
|
149
|
+
filePath: this.options.testApp,
|
|
150
|
+
url: `${this.URL}/${this.appId}/tests`,
|
|
151
|
+
credentials: this.credentials,
|
|
152
|
+
contentType: 'application/vnd.android.package-archive',
|
|
153
|
+
showProgress: !this.options.quiet,
|
|
154
|
+
});
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
async runTests() {
|
|
158
|
+
try {
|
|
159
|
+
const capabilities = this.options.getCapabilities();
|
|
160
|
+
const espressoOptions = this.options.getEspressoOptions();
|
|
161
|
+
const response = await axios_1.default.post(`${this.URL}/${this.appId}/run`, {
|
|
162
|
+
capabilities: [capabilities],
|
|
163
|
+
...(espressoOptions && { espressoOptions }),
|
|
164
|
+
}, {
|
|
165
|
+
headers: {
|
|
166
|
+
'Content-Type': 'application/json',
|
|
167
|
+
'User-Agent': utils_1.default.getUserAgent(),
|
|
168
|
+
},
|
|
169
|
+
auth: {
|
|
170
|
+
username: this.credentials.userName,
|
|
171
|
+
password: this.credentials.accessKey,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
// Check for version update notification
|
|
175
|
+
const latestVersion = response.headers?.['x-testingbotctl-version'];
|
|
176
|
+
utils_1.default.checkForUpdate(latestVersion);
|
|
177
|
+
const result = response.data;
|
|
178
|
+
// Capture real-time update server info
|
|
179
|
+
if (result.update_server && result.update_key) {
|
|
180
|
+
this.updateServer = result.update_server;
|
|
181
|
+
this.updateKey = result.update_key;
|
|
182
|
+
}
|
|
183
|
+
if (result.success === false) {
|
|
184
|
+
const errorMessage = result.errors?.join('\n') || result.error || 'Unknown error';
|
|
185
|
+
throw new testingbot_error_1.default(`Running Espresso test failed`, {
|
|
186
|
+
cause: errorMessage,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
if (error instanceof testingbot_error_1.default) {
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
throw new testingbot_error_1.default(`Running Espresso test failed`, {
|
|
196
|
+
cause: error,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async getStatus() {
|
|
201
|
+
try {
|
|
202
|
+
const response = await axios_1.default.get(`${this.URL}/${this.appId}`, {
|
|
203
|
+
headers: {
|
|
204
|
+
'User-Agent': utils_1.default.getUserAgent(),
|
|
205
|
+
},
|
|
206
|
+
auth: {
|
|
207
|
+
username: this.credentials.userName,
|
|
208
|
+
password: this.credentials.accessKey,
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
return response.data;
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
throw new testingbot_error_1.default(`Failed to get Espresso test status`, {
|
|
215
|
+
cause: error,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async waitForCompletion() {
|
|
220
|
+
let attempts = 0;
|
|
221
|
+
const startTime = Date.now();
|
|
222
|
+
const previousStatus = new Map();
|
|
223
|
+
while (attempts < this.MAX_POLL_ATTEMPTS) {
|
|
224
|
+
if (this.isShuttingDown) {
|
|
225
|
+
throw new testingbot_error_1.default('Test run cancelled by user');
|
|
226
|
+
}
|
|
227
|
+
const status = await this.getStatus();
|
|
228
|
+
// Track active run IDs for graceful shutdown
|
|
229
|
+
this.activeRunIds = status.runs
|
|
230
|
+
.filter((run) => run.status !== 'DONE' && run.status !== 'FAILED')
|
|
231
|
+
.map((run) => run.id);
|
|
232
|
+
// Log current status of runs (unless quiet mode)
|
|
233
|
+
if (!this.options.quiet) {
|
|
234
|
+
this.displayRunStatus(status.runs, startTime, previousStatus);
|
|
235
|
+
}
|
|
236
|
+
if (status.completed) {
|
|
237
|
+
// Clear the updating line and print final status
|
|
238
|
+
if (!this.options.quiet) {
|
|
239
|
+
this.clearLine();
|
|
240
|
+
for (const run of status.runs) {
|
|
241
|
+
const statusEmoji = run.success === 1 ? '✅' : '❌';
|
|
242
|
+
const statusText = run.success === 1 ? 'Test completed successfully' : 'Test failed';
|
|
243
|
+
console.log(` ${statusEmoji} Run ${run.id} (${run.capabilities.deviceName}): ${statusText}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const allSucceeded = status.runs.every((run) => run.success === 1);
|
|
247
|
+
if (allSucceeded) {
|
|
248
|
+
if (!this.options.quiet) {
|
|
249
|
+
logger_1.default.info('All tests completed successfully!');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
const failedRuns = status.runs.filter((run) => run.success !== 1);
|
|
254
|
+
logger_1.default.error(`${failedRuns.length} test run(s) failed:`);
|
|
255
|
+
for (const run of failedRuns) {
|
|
256
|
+
logger_1.default.error(` - Run ${run.id} (${run.capabilities.deviceName}): ${run.report || 'No report available'}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Fetch reports if requested
|
|
260
|
+
if (this.options.report && this.options.reportOutputDir) {
|
|
261
|
+
await this.fetchReports();
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
success: status.success,
|
|
265
|
+
runs: status.runs,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
attempts++;
|
|
269
|
+
await this.sleep(this.POLL_INTERVAL_MS);
|
|
270
|
+
}
|
|
271
|
+
throw new testingbot_error_1.default(`Test timed out after ${(this.MAX_POLL_ATTEMPTS * this.POLL_INTERVAL_MS) / 1000 / 60} minutes`);
|
|
272
|
+
}
|
|
273
|
+
displayRunStatus(runs, startTime, previousStatus) {
|
|
274
|
+
const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
|
|
275
|
+
const elapsedStr = this.formatElapsedTime(elapsedSeconds);
|
|
276
|
+
for (const run of runs) {
|
|
277
|
+
const prevStatus = previousStatus.get(run.id);
|
|
278
|
+
const statusChanged = prevStatus !== run.status;
|
|
279
|
+
if (statusChanged &&
|
|
280
|
+
prevStatus &&
|
|
281
|
+
(prevStatus === 'WAITING' || prevStatus === 'READY')) {
|
|
282
|
+
this.clearLine();
|
|
283
|
+
}
|
|
284
|
+
previousStatus.set(run.id, run.status);
|
|
285
|
+
const statusInfo = this.getStatusInfo(run.status);
|
|
286
|
+
if (run.status === 'WAITING' || run.status === 'READY') {
|
|
287
|
+
const message = ` ${statusInfo.emoji} Run ${run.id} (${run.capabilities.deviceName}): ${statusInfo.text} (${elapsedStr})`;
|
|
288
|
+
process.stdout.write(`\r${message}`);
|
|
289
|
+
}
|
|
290
|
+
else if (statusChanged) {
|
|
291
|
+
console.log(` ${statusInfo.emoji} Run ${run.id} (${run.capabilities.deviceName}): ${statusInfo.text}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
clearLine() {
|
|
296
|
+
platform_1.default.clearLine();
|
|
297
|
+
}
|
|
298
|
+
formatElapsedTime(seconds) {
|
|
299
|
+
if (seconds < 60) {
|
|
300
|
+
return `${seconds}s`;
|
|
301
|
+
}
|
|
302
|
+
const minutes = Math.floor(seconds / 60);
|
|
303
|
+
const remainingSeconds = seconds % 60;
|
|
304
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
305
|
+
}
|
|
306
|
+
getStatusInfo(status) {
|
|
307
|
+
switch (status) {
|
|
308
|
+
case 'WAITING':
|
|
309
|
+
return { emoji: '⏳', text: 'Waiting for test to start' };
|
|
310
|
+
case 'READY':
|
|
311
|
+
return { emoji: '🔄', text: 'Running test' };
|
|
312
|
+
case 'DONE':
|
|
313
|
+
return { emoji: '✅', text: 'Test has finished running' };
|
|
314
|
+
case 'FAILED':
|
|
315
|
+
return { emoji: '❌', text: 'Test failed' };
|
|
316
|
+
default:
|
|
317
|
+
return { emoji: '❓', text: status };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async fetchReports() {
|
|
321
|
+
const reportFormat = this.options.report;
|
|
322
|
+
const outputDir = this.options.reportOutputDir;
|
|
323
|
+
if (!reportFormat || !outputDir) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
// Espresso only supports junit report format via the project-level /report endpoint
|
|
327
|
+
if (reportFormat !== 'junit') {
|
|
328
|
+
logger_1.default.warn('Espresso only supports junit report format');
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (!this.options.quiet) {
|
|
332
|
+
logger_1.default.info('Fetching junit report...');
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
const response = await axios_1.default.get(`${this.URL}/${this.appId}/report`, {
|
|
336
|
+
headers: {
|
|
337
|
+
'User-Agent': utils_1.default.getUserAgent(),
|
|
338
|
+
},
|
|
339
|
+
auth: {
|
|
340
|
+
username: this.credentials.userName,
|
|
341
|
+
password: this.credentials.accessKey,
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
const reportContent = response.data;
|
|
345
|
+
if (!reportContent) {
|
|
346
|
+
logger_1.default.error('No report content received');
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const fileName = `espresso_report_${this.appId}.xml`;
|
|
350
|
+
const filePath = node_path_1.default.join(outputDir, fileName);
|
|
351
|
+
await node_fs_1.default.promises.writeFile(filePath, reportContent, 'utf-8');
|
|
352
|
+
if (!this.options.quiet) {
|
|
353
|
+
logger_1.default.info(` Saved report: ${filePath}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
logger_1.default.error(`Failed to fetch report: ${error instanceof Error ? error.message : error}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
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
|
+
connectToUpdateServer() {
|
|
461
|
+
if (!this.updateServer || !this.updateKey || this.options.quiet) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
this.socket = (0, socket_io_client_1.io)(this.updateServer, {
|
|
466
|
+
transports: ['websocket'],
|
|
467
|
+
reconnection: true,
|
|
468
|
+
reconnectionAttempts: 3,
|
|
469
|
+
reconnectionDelay: 1000,
|
|
470
|
+
timeout: 10000,
|
|
471
|
+
});
|
|
472
|
+
this.socket.on('connect', () => {
|
|
473
|
+
// Join the room for this test run
|
|
474
|
+
this.socket?.emit('join', this.updateKey);
|
|
475
|
+
});
|
|
476
|
+
this.socket.on('espresso_data', (data) => {
|
|
477
|
+
this.handleEspressoData(data);
|
|
478
|
+
});
|
|
479
|
+
this.socket.on('espresso_error', (data) => {
|
|
480
|
+
this.handleEspressoError(data);
|
|
481
|
+
});
|
|
482
|
+
this.socket.on('connect_error', () => {
|
|
483
|
+
// Silently fail - real-time updates are optional
|
|
484
|
+
this.disconnectFromUpdateServer();
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
// Socket connection failed, continue without real-time updates
|
|
489
|
+
this.socket = null;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
disconnectFromUpdateServer() {
|
|
493
|
+
if (this.socket) {
|
|
494
|
+
this.socket.disconnect();
|
|
495
|
+
this.socket = null;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
handleEspressoData(data) {
|
|
499
|
+
try {
|
|
500
|
+
const message = JSON.parse(data);
|
|
501
|
+
if (message.payload) {
|
|
502
|
+
// Clear the status line before printing output
|
|
503
|
+
this.clearLine();
|
|
504
|
+
// Print the Espresso output
|
|
505
|
+
process.stdout.write(message.payload);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
// Invalid JSON, ignore
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
handleEspressoError(data) {
|
|
513
|
+
try {
|
|
514
|
+
const message = JSON.parse(data);
|
|
515
|
+
if (message.payload) {
|
|
516
|
+
// Clear the status line before printing error
|
|
517
|
+
this.clearLine();
|
|
518
|
+
// Print the error output
|
|
519
|
+
process.stderr.write(message.payload);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
// Invalid JSON, ignore
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
exports.default = Espresso;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface LoginResult {
|
|
2
|
+
success: boolean;
|
|
3
|
+
message: string;
|
|
4
|
+
}
|
|
5
|
+
export default class Login {
|
|
6
|
+
private server;
|
|
7
|
+
private port;
|
|
8
|
+
run(): Promise<LoginResult>;
|
|
9
|
+
private startServer;
|
|
10
|
+
private stopServer;
|
|
11
|
+
private waitForCallback;
|
|
12
|
+
private parseRequestBody;
|
|
13
|
+
private sendSuccessResponse;
|
|
14
|
+
private sendErrorResponse;
|
|
15
|
+
private saveCredentials;
|
|
16
|
+
private openBrowser;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=login.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/providers/login.ts"],"names":[],"mappings":"AASA,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,iBAAiB;YAwDX,eAAe;YAKf,WAAW;CAqB1B"}
|