@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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +375 -0
  3. package/dist/auth.d.ts +16 -0
  4. package/dist/auth.d.ts.map +1 -0
  5. package/dist/auth.js +47 -0
  6. package/dist/cli.d.ts +4 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +329 -0
  9. package/dist/index.d.ts +3 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +8 -0
  12. package/dist/logger.d.ts +4 -0
  13. package/dist/logger.d.ts.map +1 -0
  14. package/dist/logger.js +20 -0
  15. package/dist/models/credentials.d.ts +9 -0
  16. package/dist/models/credentials.d.ts.map +1 -0
  17. package/dist/models/credentials.js +20 -0
  18. package/dist/models/espresso_options.d.ts +116 -0
  19. package/dist/models/espresso_options.d.ts.map +1 -0
  20. package/dist/models/espresso_options.js +194 -0
  21. package/dist/models/maestro_options.d.ts +101 -0
  22. package/dist/models/maestro_options.d.ts.map +1 -0
  23. package/dist/models/maestro_options.js +176 -0
  24. package/dist/models/testingbot_error.d.ts +3 -0
  25. package/dist/models/testingbot_error.d.ts.map +1 -0
  26. package/dist/models/testingbot_error.js +5 -0
  27. package/dist/models/xcuitest_options.d.ts +88 -0
  28. package/dist/models/xcuitest_options.d.ts.map +1 -0
  29. package/dist/models/xcuitest_options.js +146 -0
  30. package/dist/providers/espresso.d.ts +67 -0
  31. package/dist/providers/espresso.d.ts.map +1 -0
  32. package/dist/providers/espresso.js +527 -0
  33. package/dist/providers/login.d.ts +18 -0
  34. package/dist/providers/login.d.ts.map +1 -0
  35. package/dist/providers/login.js +284 -0
  36. package/dist/providers/maestro.d.ts +92 -0
  37. package/dist/providers/maestro.d.ts.map +1 -0
  38. package/dist/providers/maestro.js +1010 -0
  39. package/dist/providers/xcuitest.d.ts +67 -0
  40. package/dist/providers/xcuitest.d.ts.map +1 -0
  41. package/dist/providers/xcuitest.js +529 -0
  42. package/dist/upload.d.ts +21 -0
  43. package/dist/upload.d.ts.map +1 -0
  44. package/dist/upload.js +94 -0
  45. package/dist/utils/file-type-detector.d.ts +15 -0
  46. package/dist/utils/file-type-detector.d.ts.map +1 -0
  47. package/dist/utils/file-type-detector.js +38 -0
  48. package/dist/utils/platform.d.ts +26 -0
  49. package/dist/utils/platform.d.ts.map +1 -0
  50. package/dist/utils/platform.js +58 -0
  51. package/dist/utils.d.ts +15 -0
  52. package/dist/utils.d.ts.map +1 -0
  53. package/dist/utils.js +48 -0
  54. package/package.json +78 -0
@@ -0,0 +1,67 @@
1
+ import XCUITestOptions from '../models/xcuitest_options';
2
+ import Credentials from '../models/credentials';
3
+ export interface XCUITestRunInfo {
4
+ id: number;
5
+ status: 'WAITING' | 'READY' | 'DONE' | 'FAILED';
6
+ capabilities: {
7
+ deviceName: string;
8
+ platformName: string;
9
+ version?: string;
10
+ };
11
+ success: number;
12
+ report?: string;
13
+ }
14
+ export interface XCUITestStatusResponse {
15
+ runs: XCUITestRunInfo[];
16
+ success: boolean;
17
+ completed: boolean;
18
+ }
19
+ export interface XCUITestResult {
20
+ success: boolean;
21
+ runs: XCUITestRunInfo[];
22
+ }
23
+ export interface XCUITestSocketMessage {
24
+ id: number;
25
+ payload: string;
26
+ }
27
+ export default class XCUITest {
28
+ private readonly URL;
29
+ private readonly POLL_INTERVAL_MS;
30
+ private readonly MAX_POLL_ATTEMPTS;
31
+ private credentials;
32
+ private options;
33
+ private upload;
34
+ private appId;
35
+ private activeRunIds;
36
+ private isShuttingDown;
37
+ private signalHandler;
38
+ private socket;
39
+ private updateServer;
40
+ private updateKey;
41
+ constructor(credentials: Credentials, options: XCUITestOptions);
42
+ private validate;
43
+ private ensureOutputDirectory;
44
+ run(): Promise<XCUITestResult>;
45
+ private uploadApp;
46
+ private uploadTestApp;
47
+ private runTests;
48
+ private getStatus;
49
+ private waitForCompletion;
50
+ private displayRunStatus;
51
+ private clearLine;
52
+ private formatElapsedTime;
53
+ private getStatusInfo;
54
+ private fetchReports;
55
+ private sleep;
56
+ private extractErrorMessage;
57
+ private setupSignalHandlers;
58
+ private removeSignalHandlers;
59
+ private handleShutdown;
60
+ private stopActiveRuns;
61
+ private stopRun;
62
+ private connectToUpdateServer;
63
+ private disconnectFromUpdateServer;
64
+ private handleXCUITestData;
65
+ private handleXCUITestError;
66
+ }
67
+ //# sourceMappingURL=xcuitest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xcuitest.d.ts","sourceRoot":"","sources":["../../src/providers/xcuitest.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,MAAM,4BAA4B,CAAC;AAEzD,OAAO,WAAW,MAAM,uBAAuB,CAAC;AAUhD,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;IAChD,YAAY,EAAE;QACZ,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;QACrB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,eAAe,EAAE,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,eAAe,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,CAAC,OAAO,OAAO,QAAQ;IAC3B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAyD;IAC7E,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAQ;IACzC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAO;IAEzC,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,MAAM,CAAS;IAEvB,OAAO,CAAC,KAAK,CAAiC;IAC9C,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,SAAS,CAAuB;gBAErB,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,eAAe;YAMvD,QAAQ;YAuCR,qBAAqB;IA6BtB,GAAG,IAAI,OAAO,CAAC,cAAc,CAAC;YA4D7B,SAAS;YAaT,aAAa;YAYb,QAAQ;YAsDR,SAAS;YAoBT,iBAAiB;IAwE/B,OAAO,CAAC,gBAAgB;IAmCxB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,aAAa;YAkBP,YAAY;IAsD1B,OAAO,CAAC,KAAK;IAIb,OAAO,CAAC,mBAAmB;IAiD3B,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,oBAAoB;IAO5B,OAAO,CAAC,cAAc;YAuBR,cAAc;YAgBd,OAAO;IA8BrB,OAAO,CAAC,qBAAqB;IAqC7B,OAAO,CAAC,0BAA0B;IAOlC,OAAO,CAAC,kBAAkB;IAc1B,OAAO,CAAC,mBAAmB;CAa5B"}
@@ -0,0 +1,529 @@
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 XCUITest {
16
+ URL = 'https://api.testingbot.com/v1/app-automate/xcuitest';
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 XCUITest App');
93
+ }
94
+ await this.uploadApp();
95
+ if (!this.options.quiet) {
96
+ logger_1.default.info('Uploading XCUITest Test App');
97
+ }
98
+ await this.uploadTestApp();
99
+ if (!this.options.quiet) {
100
+ logger_1.default.info('Running XCUITests');
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/octet-stream',
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/zip',
153
+ showProgress: !this.options.quiet,
154
+ });
155
+ return true;
156
+ }
157
+ async runTests() {
158
+ try {
159
+ const capabilities = this.options.getCapabilities();
160
+ const xcuitestOptions = this.options.getXCUITestOptions();
161
+ const response = await axios_1.default.post(`${this.URL}/${this.appId}/run`, {
162
+ capabilities: [capabilities],
163
+ ...(xcuitestOptions && { options: xcuitestOptions }),
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 XCUITest 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 XCUITest 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 XCUITest 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(status.runs);
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(runs) {
321
+ const reportFormat = this.options.report;
322
+ const outputDir = this.options.reportOutputDir;
323
+ if (!reportFormat || !outputDir) {
324
+ return;
325
+ }
326
+ if (!this.options.quiet) {
327
+ logger_1.default.info(`Fetching ${reportFormat} report(s)...`);
328
+ }
329
+ for (const run of runs) {
330
+ try {
331
+ const endpoint = reportFormat === 'junit'
332
+ ? `${this.URL}/${this.appId}/${run.id}/junit_report`
333
+ : `${this.URL}/${this.appId}/${run.id}/html_report`;
334
+ const response = await axios_1.default.get(endpoint, {
335
+ headers: {
336
+ 'User-Agent': utils_1.default.getUserAgent(),
337
+ },
338
+ auth: {
339
+ username: this.credentials.userName,
340
+ password: this.credentials.accessKey,
341
+ },
342
+ responseType: reportFormat === 'html' ? 'arraybuffer' : 'text',
343
+ });
344
+ const reportContent = response.data;
345
+ if (!reportContent) {
346
+ logger_1.default.error(`No report content received for run ${run.id}`);
347
+ continue;
348
+ }
349
+ const extension = reportFormat === 'junit' ? 'xml' : 'html';
350
+ const fileName = `xcuitest_report_${this.appId}_${run.id}.${extension}`;
351
+ const filePath = node_path_1.default.join(outputDir, fileName);
352
+ await node_fs_1.default.promises.writeFile(filePath, reportContent);
353
+ if (!this.options.quiet) {
354
+ logger_1.default.info(` Saved report: ${filePath}`);
355
+ }
356
+ }
357
+ catch (error) {
358
+ logger_1.default.error(`Failed to fetch report for run ${run.id}: ${error instanceof Error ? error.message : error}`);
359
+ }
360
+ }
361
+ }
362
+ sleep(ms) {
363
+ return new Promise((resolve) => setTimeout(resolve, ms));
364
+ }
365
+ extractErrorMessage(cause) {
366
+ if (typeof cause === 'string') {
367
+ return cause;
368
+ }
369
+ if (Array.isArray(cause)) {
370
+ return cause.join('\n');
371
+ }
372
+ if (cause && typeof cause === 'object') {
373
+ const axiosError = cause;
374
+ if (axiosError.response?.data?.errors) {
375
+ return axiosError.response.data.errors.join('\n');
376
+ }
377
+ if (axiosError.response?.data?.error) {
378
+ return axiosError.response.data.error;
379
+ }
380
+ if (axiosError.response?.data?.message) {
381
+ return axiosError.response.data.message;
382
+ }
383
+ if (cause instanceof Error) {
384
+ return cause.message;
385
+ }
386
+ const obj = cause;
387
+ if (obj.errors) {
388
+ return obj.errors.join('\n');
389
+ }
390
+ if (obj.error) {
391
+ return obj.error;
392
+ }
393
+ if (obj.message) {
394
+ return obj.message;
395
+ }
396
+ }
397
+ return null;
398
+ }
399
+ setupSignalHandlers() {
400
+ this.signalHandler = () => {
401
+ this.handleShutdown();
402
+ };
403
+ platform_1.default.setupSignalHandlers(this.signalHandler);
404
+ }
405
+ removeSignalHandlers() {
406
+ if (this.signalHandler) {
407
+ platform_1.default.removeSignalHandlers(this.signalHandler);
408
+ this.signalHandler = null;
409
+ }
410
+ }
411
+ handleShutdown() {
412
+ if (this.isShuttingDown) {
413
+ logger_1.default.warn('Force exiting...');
414
+ process.exit(1);
415
+ }
416
+ this.isShuttingDown = true;
417
+ this.clearLine();
418
+ logger_1.default.warn('Received interrupt signal, stopping test runs...');
419
+ this.stopActiveRuns()
420
+ .then(() => {
421
+ logger_1.default.info('All test runs have been stopped.');
422
+ process.exit(1);
423
+ })
424
+ .catch((error) => {
425
+ logger_1.default.error(`Failed to stop some test runs: ${error instanceof Error ? error.message : error}`);
426
+ process.exit(1);
427
+ });
428
+ }
429
+ async stopActiveRuns() {
430
+ if (!this.appId || this.activeRunIds.length === 0) {
431
+ return;
432
+ }
433
+ const stopPromises = this.activeRunIds.map((runId) => this.stopRun(runId).catch((error) => {
434
+ logger_1.default.error(`Failed to stop run ${runId}: ${error instanceof Error ? error.message : error}`);
435
+ }));
436
+ await Promise.all(stopPromises);
437
+ }
438
+ async stopRun(runId) {
439
+ if (!this.appId) {
440
+ return;
441
+ }
442
+ try {
443
+ await axios_1.default.post(`${this.URL}/${this.appId}/${runId}/stop`, {}, {
444
+ headers: {
445
+ 'User-Agent': utils_1.default.getUserAgent(),
446
+ },
447
+ auth: {
448
+ username: this.credentials.userName,
449
+ password: this.credentials.accessKey,
450
+ },
451
+ });
452
+ if (!this.options.quiet) {
453
+ logger_1.default.info(` Stopped run ${runId}`);
454
+ }
455
+ }
456
+ catch (error) {
457
+ throw new testingbot_error_1.default(`Failed to stop run ${runId}`, {
458
+ cause: error,
459
+ });
460
+ }
461
+ }
462
+ connectToUpdateServer() {
463
+ if (!this.updateServer || !this.updateKey || this.options.quiet) {
464
+ return;
465
+ }
466
+ try {
467
+ this.socket = (0, socket_io_client_1.io)(this.updateServer, {
468
+ transports: ['websocket'],
469
+ reconnection: true,
470
+ reconnectionAttempts: 3,
471
+ reconnectionDelay: 1000,
472
+ timeout: 10000,
473
+ });
474
+ this.socket.on('connect', () => {
475
+ // Join the room for this test run
476
+ this.socket?.emit('join', this.updateKey);
477
+ });
478
+ this.socket.on('xcuitest_data', (data) => {
479
+ this.handleXCUITestData(data);
480
+ });
481
+ this.socket.on('xcuitest_error', (data) => {
482
+ this.handleXCUITestError(data);
483
+ });
484
+ this.socket.on('connect_error', () => {
485
+ // Silently fail - real-time updates are optional
486
+ this.disconnectFromUpdateServer();
487
+ });
488
+ }
489
+ catch {
490
+ // Socket connection failed, continue without real-time updates
491
+ this.socket = null;
492
+ }
493
+ }
494
+ disconnectFromUpdateServer() {
495
+ if (this.socket) {
496
+ this.socket.disconnect();
497
+ this.socket = null;
498
+ }
499
+ }
500
+ handleXCUITestData(data) {
501
+ try {
502
+ const message = JSON.parse(data);
503
+ if (message.payload) {
504
+ // Clear the status line before printing output
505
+ this.clearLine();
506
+ // Print the XCUITest output
507
+ process.stdout.write(message.payload);
508
+ }
509
+ }
510
+ catch {
511
+ // Invalid JSON, ignore
512
+ }
513
+ }
514
+ handleXCUITestError(data) {
515
+ try {
516
+ const message = JSON.parse(data);
517
+ if (message.payload) {
518
+ // Clear the status line before printing error
519
+ this.clearLine();
520
+ // Print the error output
521
+ process.stderr.write(message.payload);
522
+ }
523
+ }
524
+ catch {
525
+ // Invalid JSON, ignore
526
+ }
527
+ }
528
+ }
529
+ exports.default = XCUITest;
@@ -0,0 +1,21 @@
1
+ import Credentials from './models/credentials';
2
+ export type ContentType = 'application/vnd.android.package-archive' | 'application/octet-stream' | 'application/zip';
3
+ export interface UploadOptions {
4
+ filePath: string;
5
+ url: string;
6
+ credentials: Credentials;
7
+ contentType: ContentType;
8
+ showProgress?: boolean;
9
+ }
10
+ export interface UploadResult {
11
+ id: number;
12
+ }
13
+ export default class Upload {
14
+ private lastProgressPercent;
15
+ upload(options: UploadOptions): Promise<UploadResult>;
16
+ private validateFile;
17
+ private handleProgress;
18
+ private displayProgress;
19
+ private clearProgressLine;
20
+ }
21
+ //# sourceMappingURL=upload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AAIA,OAAO,WAAW,MAAM,sBAAsB,CAAC;AAI/C,MAAM,MAAM,WAAW,GACnB,yCAAyC,GACzC,0BAA0B,GAC1B,iBAAiB,CAAC;AAEtB,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,WAAW,CAAC;IACzB,WAAW,EAAE,WAAW,CAAC;IACzB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,CAAC,OAAO,OAAO,MAAM;IACzB,OAAO,CAAC,mBAAmB,CAAa;IAE3B,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;YA+DpD,YAAY;IAQ1B,OAAO,CAAC,cAAc;IAetB,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,iBAAiB;CAI1B"}