@testingbot/cli 1.0.0 → 1.0.2

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 (42) hide show
  1. package/README.md +3 -2
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +67 -8
  4. package/dist/models/espresso_options.d.ts +9 -0
  5. package/dist/models/espresso_options.d.ts.map +1 -1
  6. package/dist/models/espresso_options.js +14 -0
  7. package/dist/models/maestro_options.d.ts +19 -7
  8. package/dist/models/maestro_options.d.ts.map +1 -1
  9. package/dist/models/maestro_options.js +17 -8
  10. package/dist/models/testingbot_error.d.ts +3 -0
  11. package/dist/models/testingbot_error.d.ts.map +1 -1
  12. package/dist/models/testingbot_error.js +5 -0
  13. package/dist/models/xcuitest_options.d.ts +9 -0
  14. package/dist/models/xcuitest_options.d.ts.map +1 -1
  15. package/dist/models/xcuitest_options.js +14 -0
  16. package/dist/providers/base_provider.d.ts +119 -0
  17. package/dist/providers/base_provider.d.ts.map +1 -0
  18. package/dist/providers/base_provider.js +296 -0
  19. package/dist/providers/espresso.d.ts +14 -21
  20. package/dist/providers/espresso.d.ts.map +1 -1
  21. package/dist/providers/espresso.js +39 -168
  22. package/dist/providers/login.d.ts +1 -0
  23. package/dist/providers/login.d.ts.map +1 -1
  24. package/dist/providers/login.js +17 -8
  25. package/dist/providers/maestro.d.ts +54 -22
  26. package/dist/providers/maestro.d.ts.map +1 -1
  27. package/dist/providers/maestro.js +683 -286
  28. package/dist/providers/xcuitest.d.ts +14 -21
  29. package/dist/providers/xcuitest.d.ts.map +1 -1
  30. package/dist/providers/xcuitest.js +39 -168
  31. package/dist/upload.d.ts +11 -4
  32. package/dist/upload.d.ts.map +1 -1
  33. package/dist/upload.js +80 -35
  34. package/dist/utils/connectivity.d.ts +25 -0
  35. package/dist/utils/connectivity.d.ts.map +1 -0
  36. package/dist/utils/connectivity.js +118 -0
  37. package/dist/utils/error-helpers.d.ts +26 -0
  38. package/dist/utils/error-helpers.d.ts.map +1 -0
  39. package/dist/utils/error-helpers.js +237 -0
  40. package/dist/utils.d.ts.map +1 -1
  41. package/dist/utils.js +7 -2
  42. package/package.json +3 -1
@@ -1,5 +1,11 @@
1
1
  import XCUITestOptions from '../models/xcuitest_options';
2
2
  import Credentials from '../models/credentials';
3
+ import BaseProvider from './base_provider';
4
+ export interface XCUITestRunEnvironment {
5
+ device?: string;
6
+ name?: string;
7
+ version?: string;
8
+ }
3
9
  export interface XCUITestRunInfo {
4
10
  id: number;
5
11
  status: 'WAITING' | 'READY' | 'DONE' | 'FAILED';
@@ -8,6 +14,7 @@ export interface XCUITestRunInfo {
8
14
  platformName: string;
9
15
  version?: string;
10
16
  };
17
+ environment?: XCUITestRunEnvironment;
11
18
  success: number;
12
19
  report?: string;
13
20
  }
@@ -24,23 +31,13 @@ export interface XCUITestSocketMessage {
24
31
  id: number;
25
32
  payload: string;
26
33
  }
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;
34
+ export default class XCUITest extends BaseProvider<XCUITestOptions> {
35
+ protected readonly URL = "https://api.testingbot.com/v1/app-automate/xcuitest";
38
36
  private socket;
39
37
  private updateServer;
40
38
  private updateKey;
41
39
  constructor(credentials: Credentials, options: XCUITestOptions);
42
40
  private validate;
43
- private ensureOutputDirectory;
44
41
  run(): Promise<XCUITestResult>;
45
42
  private uploadApp;
46
43
  private uploadTestApp;
@@ -48,17 +45,13 @@ export default class XCUITest {
48
45
  private getStatus;
49
46
  private waitForCompletion;
50
47
  private displayRunStatus;
51
- private clearLine;
52
- private formatElapsedTime;
48
+ /**
49
+ * Get the display name for a run, preferring environment.name over capabilities.deviceName
50
+ * This shows the actual device used when a wildcard (*) was specified
51
+ */
52
+ private getRunDisplayName;
53
53
  private getStatusInfo;
54
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
55
  private connectToUpdateServer;
63
56
  private disconnectFromUpdateServer;
64
57
  private handleXCUITestData;
@@ -1 +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"}
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;AAOhD,OAAO,YAAY,MAAM,iBAAiB,CAAC;AAE3C,MAAM,WAAW,sBAAsB;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,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,WAAW,CAAC,EAAE,sBAAsB,CAAC;IACrC,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,QAAS,SAAQ,YAAY,CAAC,eAAe,CAAC;IACjE,SAAS,CAAC,QAAQ,CAAC,GAAG,yDACkC;IAExD,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,SAAS,CAAuB;gBAErB,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,eAAe;YAIvD,QAAQ;IAuCT,GAAG,IAAI,OAAO,CAAC,cAAc,CAAC;YA+D7B,SAAS;YAaT,aAAa;YAYb,QAAQ;YA0DR,SAAS;YA4BT,iBAAiB;IAwE/B,OAAO,CAAC,gBAAgB;IAmCxB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,aAAa;YAkBP,YAAY;IA2D1B,OAAO,CAAC,qBAAqB;IAqC7B,OAAO,CAAC,0BAA0B;IAOlC,OAAO,CAAC,kBAAkB;IAc1B,OAAO,CAAC,mBAAmB;CAa5B"}
@@ -10,26 +10,14 @@ 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 upload_1 = __importDefault(require("../upload"));
14
- const platform_1 = __importDefault(require("../utils/platform"));
15
- class XCUITest {
13
+ const base_provider_1 = __importDefault(require("./base_provider"));
14
+ class XCUITest extends base_provider_1.default {
16
15
  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
16
  socket = null;
27
17
  updateServer = null;
28
18
  updateKey = null;
29
19
  constructor(credentials, options) {
30
- this.credentials = credentials;
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) {
@@ -59,35 +47,13 @@ class XCUITest {
59
47
  }
60
48
  return true;
61
49
  }
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
50
  async run() {
87
51
  if (!(await this.validate())) {
88
52
  return { success: false, runs: [] };
89
53
  }
90
54
  try {
55
+ // Quick connectivity check before starting uploads
56
+ await this.ensureConnectivity();
91
57
  if (!this.options.quiet) {
92
58
  logger_1.default.info('Uploading XCUITest App');
93
59
  }
@@ -158,9 +124,11 @@ class XCUITest {
158
124
  try {
159
125
  const capabilities = this.options.getCapabilities();
160
126
  const xcuitestOptions = this.options.getXCUITestOptions();
127
+ const metadata = this.options.metadata;
161
128
  const response = await axios_1.default.post(`${this.URL}/${this.appId}/run`, {
162
129
  capabilities: [capabilities],
163
130
  ...(xcuitestOptions && { options: xcuitestOptions }),
131
+ ...(metadata && { metadata }),
164
132
  }, {
165
133
  headers: {
166
134
  'Content-Type': 'application/json',
@@ -170,6 +138,7 @@ class XCUITest {
170
138
  username: this.credentials.userName,
171
139
  password: this.credentials.accessKey,
172
140
  },
141
+ timeout: 30000, // 30 second timeout
173
142
  });
174
143
  // Check for version update notification
175
144
  const latestVersion = response.headers?.['x-testingbotctl-version'];
@@ -192,28 +161,30 @@ class XCUITest {
192
161
  if (error instanceof testingbot_error_1.default) {
193
162
  throw error;
194
163
  }
195
- throw new testingbot_error_1.default(`Running XCUITest failed`, {
196
- cause: error,
197
- });
164
+ throw await this.handleErrorWithDiagnostics(error, 'Running XCUITest failed');
198
165
  }
199
166
  }
200
167
  async getStatus() {
201
168
  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
- },
169
+ return await this.withRetry('Getting XCUITest status', async () => {
170
+ const response = await axios_1.default.get(`${this.URL}/${this.appId}`, {
171
+ headers: {
172
+ 'User-Agent': utils_1.default.getUserAgent(),
173
+ },
174
+ auth: {
175
+ username: this.credentials.userName,
176
+ password: this.credentials.accessKey,
177
+ },
178
+ timeout: 30000, // 30 second timeout
179
+ });
180
+ // Check for version update notification
181
+ const latestVersion = response.headers?.['x-testingbotctl-version'];
182
+ utils_1.default.checkForUpdate(latestVersion);
183
+ return response.data;
210
184
  });
211
- return response.data;
212
185
  }
213
186
  catch (error) {
214
- throw new testingbot_error_1.default(`Failed to get XCUITest status`, {
215
- cause: error,
216
- });
187
+ throw await this.handleErrorWithDiagnostics(error, 'Failed to get XCUITest status');
217
188
  }
218
189
  }
219
190
  async waitForCompletion() {
@@ -240,7 +211,7 @@ class XCUITest {
240
211
  for (const run of status.runs) {
241
212
  const statusEmoji = run.success === 1 ? '✅' : '❌';
242
213
  const statusText = run.success === 1 ? 'Test completed successfully' : 'Test failed';
243
- console.log(` ${statusEmoji} Run ${run.id} (${run.capabilities.deviceName}): ${statusText}`);
214
+ console.log(` ${statusEmoji} Run ${run.id} (${this.getRunDisplayName(run)}): ${statusText}`);
244
215
  }
245
216
  }
246
217
  const allSucceeded = status.runs.every((run) => run.success === 1);
@@ -253,7 +224,7 @@ class XCUITest {
253
224
  const failedRuns = status.runs.filter((run) => run.success !== 1);
254
225
  logger_1.default.error(`${failedRuns.length} test run(s) failed:`);
255
226
  for (const run of failedRuns) {
256
- logger_1.default.error(` - Run ${run.id} (${run.capabilities.deviceName}): ${run.report || 'No report available'}`);
227
+ logger_1.default.error(` - Run ${run.id} (${this.getRunDisplayName(run)}): ${run.report || 'No report available'}`);
257
228
  }
258
229
  }
259
230
  // Fetch reports if requested
@@ -284,24 +255,20 @@ class XCUITest {
284
255
  previousStatus.set(run.id, run.status);
285
256
  const statusInfo = this.getStatusInfo(run.status);
286
257
  if (run.status === 'WAITING' || run.status === 'READY') {
287
- const message = ` ${statusInfo.emoji} Run ${run.id} (${run.capabilities.deviceName}): ${statusInfo.text} (${elapsedStr})`;
258
+ const message = ` ${statusInfo.emoji} Run ${run.id} (${this.getRunDisplayName(run)}): ${statusInfo.text} (${elapsedStr})`;
288
259
  process.stdout.write(`\r${message}`);
289
260
  }
290
261
  else if (statusChanged) {
291
- console.log(` ${statusInfo.emoji} Run ${run.id} (${run.capabilities.deviceName}): ${statusInfo.text}`);
262
+ console.log(` ${statusInfo.emoji} Run ${run.id} (${this.getRunDisplayName(run)}): ${statusInfo.text}`);
292
263
  }
293
264
  }
294
265
  }
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`;
266
+ /**
267
+ * Get the display name for a run, preferring environment.name over capabilities.deviceName
268
+ * This shows the actual device used when a wildcard (*) was specified
269
+ */
270
+ getRunDisplayName(run) {
271
+ return run.environment?.name || run.capabilities.deviceName;
305
272
  }
306
273
  getStatusInfo(status) {
307
274
  switch (status) {
@@ -340,7 +307,11 @@ class XCUITest {
340
307
  password: this.credentials.accessKey,
341
308
  },
342
309
  responseType: reportFormat === 'html' ? 'arraybuffer' : 'text',
310
+ timeout: 30000, // 30 second timeout
343
311
  });
312
+ // Check for version update notification
313
+ const latestVersion = response.headers?.['x-testingbotctl-version'];
314
+ utils_1.default.checkForUpdate(latestVersion);
344
315
  const reportContent = response.data;
345
316
  if (!reportContent) {
346
317
  logger_1.default.error(`No report content received for run ${run.id}`);
@@ -359,106 +330,6 @@ class XCUITest {
359
330
  }
360
331
  }
361
332
  }
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
333
  connectToUpdateServer() {
463
334
  if (!this.updateServer || !this.updateKey || this.options.quiet) {
464
335
  return;
package/dist/upload.d.ts CHANGED
@@ -6,16 +6,23 @@ export interface UploadOptions {
6
6
  credentials: Credentials;
7
7
  contentType: ContentType;
8
8
  showProgress?: boolean;
9
+ checksum?: string;
9
10
  }
10
11
  export interface UploadResult {
11
12
  id: number;
12
13
  }
13
14
  export default class Upload {
14
- private lastProgressPercent;
15
15
  upload(options: UploadOptions): Promise<UploadResult>;
16
+ private drawProgressBar;
17
+ /**
18
+ * Format file size in human-readable format (KB for small files, MB for larger)
19
+ */
20
+ private formatFileSize;
16
21
  private validateFile;
17
- private handleProgress;
18
- private displayProgress;
19
- private clearProgressLine;
22
+ /**
23
+ * Calculate MD5 checksum of a file, returning base64-encoded result
24
+ * This matches ActiveStorage's checksum format
25
+ */
26
+ calculateChecksum(filePath: string): Promise<string>;
20
27
  }
21
28
  //# sourceMappingURL=upload.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AAMA,OAAO,WAAW,MAAM,sBAAsB,CAAC;AAK/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;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,CAAC,OAAO,OAAO,MAAM;IACZ,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;IA6FlE,OAAO,CAAC,eAAe;IAmBvB;;OAEG;IACH,OAAO,CAAC,cAAc;YAUR,YAAY;IAQ1B;;;OAGG;IACU,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAUlE"}
package/dist/upload.js CHANGED
@@ -4,26 +4,51 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const axios_1 = __importDefault(require("axios"));
7
+ const node_crypto_1 = __importDefault(require("node:crypto"));
7
8
  const node_fs_1 = __importDefault(require("node:fs"));
8
9
  const node_path_1 = __importDefault(require("node:path"));
9
10
  const form_data_1 = __importDefault(require("form-data"));
11
+ const progress_stream_1 = __importDefault(require("progress-stream"));
10
12
  const testingbot_error_1 = __importDefault(require("./models/testingbot_error"));
11
13
  const utils_1 = __importDefault(require("./utils"));
14
+ const error_helpers_1 = require("./utils/error-helpers");
12
15
  class Upload {
13
- lastProgressPercent = 0;
14
16
  async upload(options) {
15
- const { filePath, url, credentials, contentType, showProgress = false, } = options;
17
+ const { filePath, url, credentials, showProgress = false } = options;
16
18
  await this.validateFile(filePath);
17
19
  const fileName = node_path_1.default.basename(filePath);
18
20
  const fileStats = await node_fs_1.default.promises.stat(filePath);
21
+ const totalSize = fileStats.size;
22
+ const progressTracker = (0, progress_stream_1.default)({
23
+ length: totalSize,
24
+ time: 100, // Emit progress every 100ms
25
+ });
26
+ let lastPercent = 0;
27
+ if (showProgress) {
28
+ this.drawProgressBar(fileName, totalSize, 0);
29
+ progressTracker.on('progress', (prog) => {
30
+ const percent = Math.round(prog.percentage);
31
+ if (percent !== lastPercent) {
32
+ lastPercent = percent;
33
+ this.drawProgressBar(fileName, totalSize, percent);
34
+ }
35
+ });
36
+ }
19
37
  const fileStream = node_fs_1.default.createReadStream(filePath);
38
+ const trackedStream = fileStream.pipe(progressTracker);
20
39
  const formData = new form_data_1.default();
21
- formData.append('file', fileStream);
40
+ formData.append('file', trackedStream, {
41
+ filename: fileName,
42
+ contentType: options.contentType,
43
+ knownLength: totalSize,
44
+ });
45
+ if (options.checksum) {
46
+ formData.append('checksum', options.checksum);
47
+ }
22
48
  try {
23
49
  const response = await axios_1.default.post(url, formData, {
24
50
  headers: {
25
- 'Content-Type': contentType,
26
- 'Content-Disposition': `attachment; filename=${fileName}`,
51
+ ...formData.getHeaders(),
27
52
  'User-Agent': utils_1.default.getUserAgent(),
28
53
  },
29
54
  auth: {
@@ -32,32 +57,61 @@ class Upload {
32
57
  },
33
58
  maxContentLength: Infinity,
34
59
  maxBodyLength: Infinity,
35
- onUploadProgress: showProgress
36
- ? (progressEvent) => {
37
- this.handleProgress(progressEvent, fileStats.size, fileName);
38
- }
39
- : undefined,
60
+ maxRedirects: 0, // Recommended for stream uploads to avoid buffering
40
61
  });
62
+ // Check for version update notification
63
+ const latestVersion = response.headers?.['x-testingbotctl-version'];
64
+ utils_1.default.checkForUpdate(latestVersion);
41
65
  const result = response.data;
42
66
  if (result.id) {
43
67
  if (showProgress) {
44
- this.clearProgressLine();
68
+ this.drawProgressBar(fileName, totalSize, 100);
69
+ console.log('');
45
70
  }
46
71
  return { id: result.id };
47
72
  }
48
73
  else {
74
+ if (showProgress) {
75
+ console.log(' Failed');
76
+ }
49
77
  throw new testingbot_error_1.default(`Upload failed: ${result.error || 'Unknown error'}`);
50
78
  }
51
79
  }
52
80
  catch (error) {
81
+ if (showProgress) {
82
+ console.log(' Failed');
83
+ }
53
84
  if (error instanceof testingbot_error_1.default) {
54
85
  throw error;
55
86
  }
56
87
  if (axios_1.default.isAxiosError(error)) {
57
- const message = error.response?.data?.error || error.message;
58
- throw new testingbot_error_1.default(`Upload failed: ${message}`);
88
+ throw (0, error_helpers_1.handleAxiosError)(error, 'Upload failed');
59
89
  }
60
- throw new testingbot_error_1.default(`Upload failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
90
+ throw new testingbot_error_1.default(`Upload failed: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error instanceof Error ? error : undefined });
91
+ }
92
+ }
93
+ drawProgressBar(fileName, totalBytes, percent) {
94
+ const barWidth = 30;
95
+ const filled = Math.round((barWidth * percent) / 100);
96
+ const empty = barWidth - filled;
97
+ const bar = '█'.repeat(filled) + '░'.repeat(empty);
98
+ const transferredBytes = (percent / 100) * totalBytes;
99
+ const transferred = this.formatFileSize(transferredBytes);
100
+ const total = this.formatFileSize(totalBytes);
101
+ process.stdout.write(`\r ${fileName}: [${bar}] ${percent}% (${transferred}/${total})`);
102
+ }
103
+ /**
104
+ * Format file size in human-readable format (KB for small files, MB for larger)
105
+ */
106
+ formatFileSize(bytes) {
107
+ if (bytes < 1024) {
108
+ return `${bytes} B`;
109
+ }
110
+ else if (bytes < 1024 * 1024) {
111
+ return `${(bytes / 1024).toFixed(1)} KB`;
112
+ }
113
+ else {
114
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
61
115
  }
62
116
  }
63
117
  async validateFile(filePath) {
@@ -68,27 +122,18 @@ class Upload {
68
122
  throw new testingbot_error_1.default(`File not found or not readable: ${filePath}`);
69
123
  }
70
124
  }
71
- handleProgress(progressEvent, totalSize, fileName) {
72
- const loaded = progressEvent.loaded;
73
- const total = progressEvent.total || totalSize;
74
- const percent = Math.round((loaded / total) * 100);
75
- if (percent !== this.lastProgressPercent) {
76
- this.lastProgressPercent = percent;
77
- this.displayProgress(fileName, percent, loaded, total);
78
- }
79
- }
80
- displayProgress(fileName, percent, loaded, total) {
81
- const barWidth = 30;
82
- const filledWidth = Math.round((percent / 100) * barWidth);
83
- const emptyWidth = barWidth - filledWidth;
84
- const bar = '█'.repeat(filledWidth) + '░'.repeat(emptyWidth);
85
- const loadedMB = (loaded / (1024 * 1024)).toFixed(2);
86
- const totalMB = (total / (1024 * 1024)).toFixed(2);
87
- process.stdout.write(`\r ${fileName}: [${bar}] ${percent}% (${loadedMB}/${totalMB} MB)`);
88
- }
89
- clearProgressLine() {
90
- process.stdout.write('\r' + ' '.repeat(80) + '\r');
91
- this.lastProgressPercent = 0;
125
+ /**
126
+ * Calculate MD5 checksum of a file, returning base64-encoded result
127
+ * This matches ActiveStorage's checksum format
128
+ */
129
+ async calculateChecksum(filePath) {
130
+ return new Promise((resolve, reject) => {
131
+ const hash = node_crypto_1.default.createHash('md5');
132
+ const stream = node_fs_1.default.createReadStream(filePath);
133
+ stream.on('data', (chunk) => hash.update(chunk));
134
+ stream.on('end', () => resolve(hash.digest('base64')));
135
+ stream.on('error', (err) => reject(err));
136
+ });
92
137
  }
93
138
  }
94
139
  exports.default = Upload;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Utility for checking internet connectivity using third-party endpoints
3
+ */
4
+ export interface EndpointResult {
5
+ endpoint: string;
6
+ success: boolean;
7
+ statusCode?: number;
8
+ latencyMs: number;
9
+ error?: string;
10
+ }
11
+ export interface ConnectivityCheckResult {
12
+ connected: boolean;
13
+ endpointResults: EndpointResult[];
14
+ message: string;
15
+ }
16
+ /**
17
+ * Check if the system has internet connectivity by testing against
18
+ * multiple reliable third-party endpoints with detailed diagnostics.
19
+ */
20
+ export declare function checkInternetConnectivity(): Promise<ConnectivityCheckResult>;
21
+ /**
22
+ * Format connectivity check results for display
23
+ */
24
+ export declare function formatConnectivityResults(result: ConnectivityCheckResult): string;
25
+ //# sourceMappingURL=connectivity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connectivity.d.ts","sourceRoot":"","sources":["../../src/utils/connectivity.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,cAAc,EAAE,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAsB,yBAAyB,IAAI,OAAO,CAAC,uBAAuB,CAAC,CAmFlF;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,uBAAuB,GAC9B,MAAM,CAuBR"}