@testingbot/cli 1.0.3 → 1.0.4

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.
@@ -1 +1 @@
1
- {"version":3,"file":"espresso.d.ts","sourceRoot":"","sources":["../../src/providers/espresso.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;IAwD1B,OAAO,CAAC,qBAAqB;IAqC7B,OAAO,CAAC,0BAA0B;IAOlC,OAAO,CAAC,kBAAkB;IAc1B,OAAO,CAAC,mBAAmB;CAa5B"}
1
+ {"version":3,"file":"espresso.d.ts","sourceRoot":"","sources":["../../src/providers/espresso.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;YAcT,aAAa;YAab,QAAQ;YA0DR,SAAS;YA4BT,iBAAiB;IAwE/B,OAAO,CAAC,gBAAgB;IAmCxB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,aAAa;YAkBP,YAAY;IAwD1B,OAAO,CAAC,qBAAqB;IAqC7B,OAAO,CAAC,0BAA0B;IAOlC,OAAO,CAAC,kBAAkB;IAc1B,OAAO,CAAC,mBAAmB;CAa5B"}
@@ -104,6 +104,7 @@ class Espresso extends base_provider_1.default {
104
104
  credentials: this.credentials,
105
105
  contentType: 'application/vnd.android.package-archive',
106
106
  showProgress: !this.options.quiet,
107
+ validateZipFormat: true,
107
108
  });
108
109
  this.appId = result.id;
109
110
  return true;
@@ -115,6 +116,7 @@ class Espresso extends base_provider_1.default {
115
116
  credentials: this.credentials,
116
117
  contentType: 'application/vnd.android.package-archive',
117
118
  showProgress: !this.options.quiet,
119
+ validateZipFormat: true,
118
120
  });
119
121
  return true;
120
122
  }
@@ -76,10 +76,18 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
76
76
  private detectPlatform;
77
77
  run(): Promise<MaestroResult>;
78
78
  private uploadApp;
79
+ /**
80
+ * Zip a .app bundle directory into a temporary zip file
81
+ */
82
+ private zipAppBundle;
79
83
  private checkAppChecksum;
80
84
  private uploadFlows;
81
85
  private discoverFlows;
82
86
  private discoverDependencies;
87
+ /**
88
+ * Check if a file path is a Maestro config file (config.yaml or config.yml)
89
+ */
90
+ private isConfigFile;
83
91
  /**
84
92
  * Check if a string looks like a file path (relative path with extension)
85
93
  */
@@ -1 +1 @@
1
- {"version":3,"file":"maestro.d.ts","sourceRoot":"","sources":["../../src/providers/maestro.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"maestro.d.ts","sourceRoot":"","sources":["../../src/providers/maestro.ts"],"names":[],"mappings":"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"}
@@ -172,44 +172,84 @@ class Maestro extends base_provider_1.default {
172
172
  }
173
173
  }
174
174
  async uploadApp() {
175
- const appPath = this.options.app;
175
+ let appPath = this.options.app;
176
176
  const ext = node_path_1.default.extname(appPath).toLowerCase();
177
- let contentType;
178
- if (ext === '.apk') {
179
- contentType = 'application/vnd.android.package-archive';
180
- }
181
- else if (ext === '.ipa' || ext === '.app') {
182
- contentType = 'application/octet-stream';
183
- }
184
- else if (ext === '.zip') {
185
- contentType = 'application/zip';
186
- }
187
- else {
188
- contentType = 'application/octet-stream';
189
- }
190
- if (!this.options.ignoreChecksumCheck) {
191
- const checksum = await this.upload.calculateChecksum(appPath);
192
- const existingApp = await this.checkAppChecksum(checksum);
193
- if (existingApp) {
194
- this.appId = existingApp.id;
177
+ let tempZipPath = null;
178
+ // If .app bundle (directory), zip it first
179
+ if (ext === '.app') {
180
+ const stat = await node_fs_1.default.promises.stat(appPath);
181
+ if (stat.isDirectory()) {
195
182
  if (!this.options.quiet) {
196
- logger_1.default.info(' App already uploaded, skipping upload');
183
+ logger_1.default.info('Zipping .app bundle for upload');
197
184
  }
198
- return true;
185
+ tempZipPath = await this.zipAppBundle(appPath);
186
+ appPath = tempZipPath;
199
187
  }
200
188
  }
201
- if (!this.options.quiet) {
202
- logger_1.default.info('Uploading Maestro App');
203
- }
204
- const result = await this.upload.upload({
205
- filePath: appPath,
206
- url: `${this.URL}/app`,
207
- credentials: this.credentials,
208
- contentType,
209
- showProgress: !this.options.quiet,
189
+ try {
190
+ let contentType;
191
+ if (ext === '.apk') {
192
+ contentType = 'application/vnd.android.package-archive';
193
+ }
194
+ else if (ext === '.ipa') {
195
+ contentType = 'application/octet-stream';
196
+ }
197
+ else if (ext === '.zip' || ext === '.app') {
198
+ // .app bundles are zipped, so use application/zip
199
+ contentType = 'application/zip';
200
+ }
201
+ else {
202
+ contentType = 'application/octet-stream';
203
+ }
204
+ if (!this.options.ignoreChecksumCheck) {
205
+ const checksum = await this.upload.calculateChecksum(appPath);
206
+ const existingApp = await this.checkAppChecksum(checksum);
207
+ if (existingApp) {
208
+ this.appId = existingApp.id;
209
+ if (!this.options.quiet) {
210
+ logger_1.default.info(' App already uploaded, skipping upload');
211
+ }
212
+ return true;
213
+ }
214
+ }
215
+ if (!this.options.quiet) {
216
+ logger_1.default.info('Uploading Maestro App');
217
+ }
218
+ const result = await this.upload.upload({
219
+ filePath: appPath,
220
+ url: `${this.URL}/app`,
221
+ credentials: this.credentials,
222
+ contentType,
223
+ showProgress: !this.options.quiet,
224
+ validateZipFormat: true,
225
+ });
226
+ this.appId = result.id;
227
+ return true;
228
+ }
229
+ finally {
230
+ // Clean up temporary zip file
231
+ if (tempZipPath) {
232
+ await node_fs_1.default.promises.unlink(tempZipPath).catch(() => { });
233
+ }
234
+ }
235
+ }
236
+ /**
237
+ * Zip a .app bundle directory into a temporary zip file
238
+ */
239
+ async zipAppBundle(appPath) {
240
+ const appName = node_path_1.default.basename(appPath);
241
+ const tmpDir = await node_fs_1.default.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'testingbot-app-'));
242
+ const zipPath = node_path_1.default.join(tmpDir, `${appName}.zip`);
243
+ return new Promise((resolve, reject) => {
244
+ const output = node_fs_1.default.createWriteStream(zipPath);
245
+ const archive = (0, archiver_1.default)('zip', { zlib: { level: 9 } });
246
+ output.on('close', () => resolve(zipPath));
247
+ archive.on('error', (err) => reject(err));
248
+ archive.pipe(output);
249
+ // Add the .app directory with its name preserved
250
+ archive.directory(appPath, appName);
251
+ archive.finalize();
210
252
  });
211
- this.appId = result.id;
212
- return true;
213
253
  }
214
254
  async checkAppChecksum(checksum) {
215
255
  try {
@@ -252,6 +292,7 @@ class Maestro extends base_provider_1.default {
252
292
  credentials: this.credentials,
253
293
  contentType: 'application/zip',
254
294
  showProgress: !this.options.quiet,
295
+ validateZipFormat: true,
255
296
  });
256
297
  return true;
257
298
  }
@@ -303,6 +344,14 @@ class Maestro extends base_provider_1.default {
303
344
  const baseDir = baseDirs.length === 1 ? baseDirs[0] : undefined;
304
345
  if (!this.options.quiet) {
305
346
  this.logIncludedFiles(allFlowFiles, baseDir);
347
+ // Show info about potential slow execution on specific real devices
348
+ utils_1.default.showRealDeviceFlowsInfo({
349
+ realDevice: this.options.realDevice,
350
+ device: this.options.device,
351
+ version: this.options.version,
352
+ flowCount: allFlowFiles.filter((f) => f.endsWith('.yaml') || f.endsWith('.yml')).filter((f) => !this.isConfigFile(f)).length,
353
+ shardSplit: this.options.shardSplit,
354
+ });
306
355
  }
307
356
  // Check for missing file references and warn the user
308
357
  const missingReferences = await this.findMissingReferences(allFlowFiles, allFlowFiles, baseDir);
@@ -332,15 +381,20 @@ class Maestro extends base_provider_1.default {
332
381
  withFileTypes: true,
333
382
  });
334
383
  const flowFiles = [];
335
- const configPath = node_path_1.default.join(directory, 'config.yaml');
384
+ let configPath = null;
336
385
  let config = null;
337
- // Check for config.yaml
338
- try {
339
- const configContent = await node_fs_1.default.promises.readFile(configPath, 'utf-8');
340
- config = yaml.load(configContent);
341
- }
342
- catch {
343
- // No config.yaml, that's fine
386
+ // Check for config.yaml or config.yml
387
+ for (const configName of ['config.yaml', 'config.yml']) {
388
+ const candidatePath = node_path_1.default.join(directory, configName);
389
+ try {
390
+ const configContent = await node_fs_1.default.promises.readFile(candidatePath, 'utf-8');
391
+ config = yaml.load(configContent);
392
+ configPath = candidatePath;
393
+ break; // Use the first config file found
394
+ }
395
+ catch {
396
+ // Config file doesn't exist, try next
397
+ }
344
398
  }
345
399
  // If config specifies flows, use those
346
400
  if (config?.flows && config.flows.length > 0) {
@@ -355,7 +409,7 @@ class Maestro extends base_provider_1.default {
355
409
  if (entry.isFile()) {
356
410
  const ext = node_path_1.default.extname(entry.name).toLowerCase();
357
411
  if ((ext === '.yaml' || ext === '.yml') &&
358
- entry.name !== 'config.yaml') {
412
+ !this.isConfigFile(entry.name)) {
359
413
  flowFiles.push(node_path_1.default.join(directory, entry.name));
360
414
  }
361
415
  }
@@ -367,8 +421,8 @@ class Maestro extends base_provider_1.default {
367
421
  const dependencies = await this.discoverDependencies(flowFile, directory);
368
422
  dependencies.forEach((dep) => allFiles.add(dep));
369
423
  }
370
- // Include config.yaml if it exists
371
- if (config) {
424
+ // Include config file if it exists
425
+ if (configPath) {
372
426
  allFiles.add(configPath);
373
427
  }
374
428
  return Array.from(allFiles);
@@ -400,6 +454,13 @@ class Maestro extends base_provider_1.default {
400
454
  }
401
455
  return dependencies;
402
456
  }
457
+ /**
458
+ * Check if a file path is a Maestro config file (config.yaml or config.yml)
459
+ */
460
+ isConfigFile(filePath) {
461
+ const basename = node_path_1.default.basename(filePath);
462
+ return basename === 'config.yaml' || basename === 'config.yml';
463
+ }
403
464
  /**
404
465
  * Check if a string looks like a file path (relative path with extension)
405
466
  */
@@ -770,7 +831,7 @@ class Maestro extends base_provider_1.default {
770
831
  for (const filePath of relativePaths) {
771
832
  const ext = node_path_1.default.extname(filePath).toLowerCase();
772
833
  if (ext === '.yaml' || ext === '.yml') {
773
- if (filePath === 'config.yaml' || filePath.endsWith('/config.yaml')) {
834
+ if (this.isConfigFile(filePath)) {
774
835
  groups['Config files'].push(filePath);
775
836
  }
776
837
  else {
@@ -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;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"}
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;YAcT,aAAa;YAab,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"}
@@ -104,6 +104,7 @@ class XCUITest extends base_provider_1.default {
104
104
  credentials: this.credentials,
105
105
  contentType: 'application/octet-stream',
106
106
  showProgress: !this.options.quiet,
107
+ validateZipFormat: true,
107
108
  });
108
109
  this.appId = result.id;
109
110
  return true;
@@ -115,6 +116,7 @@ class XCUITest extends base_provider_1.default {
115
116
  credentials: this.credentials,
116
117
  contentType: 'application/zip',
117
118
  showProgress: !this.options.quiet,
119
+ validateZipFormat: true,
118
120
  });
119
121
  return true;
120
122
  }
package/dist/upload.d.ts CHANGED
@@ -7,6 +7,8 @@ export interface UploadOptions {
7
7
  contentType: ContentType;
8
8
  showProgress?: boolean;
9
9
  checksum?: string;
10
+ /** Validate that the file is a valid zip-based archive (APK, IPA, ZIP) */
11
+ validateZipFormat?: boolean;
10
12
  }
11
13
  export interface UploadResult {
12
14
  id: number;
@@ -19,6 +21,15 @@ export default class Upload {
19
21
  */
20
22
  private formatFileSize;
21
23
  private validateFile;
24
+ /**
25
+ * Validate that the file is a valid zip-based archive.
26
+ * ZIP, APK, IPA files all start with the ZIP magic bytes (PK\x03\x04).
27
+ */
28
+ private validateZipFormat;
29
+ /**
30
+ * Extract error message from server response data
31
+ */
32
+ private extractErrorMessage;
22
33
  /**
23
34
  * Calculate MD5 checksum of a file, returning base64-encoded result
24
35
  * This matches ActiveStorage's checksum format
@@ -1 +1 @@
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"}
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;IAClB,0EAA0E;IAC1E,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;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;IAyGlE,OAAO,CAAC,eAAe;IAmBvB;;OAEG;IACH,OAAO,CAAC,cAAc;YAUR,YAAY;IAQ1B;;;OAGG;YACW,iBAAiB;IAqB/B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAuB3B;;;OAGG;IACU,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAUlE"}
package/dist/upload.js CHANGED
@@ -16,6 +16,9 @@ class Upload {
16
16
  async upload(options) {
17
17
  const { filePath, url, credentials, showProgress = false } = options;
18
18
  await this.validateFile(filePath);
19
+ if (options.validateZipFormat) {
20
+ await this.validateZipFormat(filePath);
21
+ }
19
22
  const fileName = node_path_1.default.basename(filePath);
20
23
  const fileStats = await node_fs_1.default.promises.stat(filePath);
21
24
  const totalSize = fileStats.size;
@@ -85,6 +88,11 @@ class Upload {
85
88
  throw error;
86
89
  }
87
90
  if (axios_1.default.isAxiosError(error)) {
91
+ // Handle 400 errors specifically for file uploads
92
+ if (error.response?.status === 400) {
93
+ const serverMessage = this.extractErrorMessage(error.response.data);
94
+ throw new testingbot_error_1.default(`Upload rejected: ${serverMessage || 'The file was not accepted by the server'}`, { cause: error });
95
+ }
88
96
  throw (0, error_helpers_1.handleAxiosError)(error, 'Upload failed');
89
97
  }
90
98
  throw new testingbot_error_1.default(`Upload failed: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error instanceof Error ? error : undefined });
@@ -122,6 +130,55 @@ class Upload {
122
130
  throw new testingbot_error_1.default(`File not found or not readable: ${filePath}`);
123
131
  }
124
132
  }
133
+ /**
134
+ * Validate that the file is a valid zip-based archive.
135
+ * ZIP, APK, IPA files all start with the ZIP magic bytes (PK\x03\x04).
136
+ */
137
+ async validateZipFormat(filePath) {
138
+ const ZIP_MAGIC_BYTES = Buffer.from([0x50, 0x4b, 0x03, 0x04]); // PK\x03\x04
139
+ const fd = await node_fs_1.default.promises.open(filePath, 'r');
140
+ try {
141
+ const buffer = Buffer.alloc(4);
142
+ const { bytesRead } = await fd.read(buffer, 0, 4, 0);
143
+ if (bytesRead < 4 || !buffer.subarray(0, 4).equals(ZIP_MAGIC_BYTES)) {
144
+ const fileName = node_path_1.default.basename(filePath);
145
+ const ext = node_path_1.default.extname(filePath).toLowerCase();
146
+ throw new testingbot_error_1.default(`Invalid file format: "${fileName}" is not a valid ${ext || 'archive'} file. ` +
147
+ `The file does not appear to be a valid zip-based archive (APK, IPA, or ZIP).`);
148
+ }
149
+ }
150
+ finally {
151
+ await fd.close();
152
+ }
153
+ }
154
+ /**
155
+ * Extract error message from server response data
156
+ */
157
+ extractErrorMessage(data) {
158
+ if (!data)
159
+ return undefined;
160
+ if (typeof data === 'string') {
161
+ try {
162
+ const parsed = JSON.parse(data);
163
+ return parsed.message || parsed.error || parsed.errors || data;
164
+ }
165
+ catch {
166
+ return data;
167
+ }
168
+ }
169
+ if (typeof data === 'object') {
170
+ const obj = data;
171
+ if (typeof obj.message === 'string')
172
+ return obj.message;
173
+ if (typeof obj.error === 'string')
174
+ return obj.error;
175
+ if (typeof obj.errors === 'string')
176
+ return obj.errors;
177
+ if (Array.isArray(obj.errors))
178
+ return obj.errors.join(', ');
179
+ }
180
+ return undefined;
181
+ }
125
182
  /**
126
183
  * Calculate MD5 checksum of a file, returning base64-encoded result
127
184
  * This matches ActiveStorage's checksum format
package/dist/utils.d.ts CHANGED
@@ -6,6 +6,24 @@ declare const _default: {
6
6
  * Returns: -1 if v1 < v2, 0 if equal, 1 if v1 > v2
7
7
  */
8
8
  compareVersions(v1: string, v2: string): number;
9
+ /**
10
+ * Check if a device specification is a wildcard or regex pattern
11
+ */
12
+ isWildcardDevice(device: string | undefined): boolean;
13
+ /**
14
+ * Check if a version specification is a wildcard or regex pattern
15
+ */
16
+ isWildcardVersion(version: string | undefined): boolean;
17
+ /**
18
+ * Show info message when running many flows on a specific real device without sharding
19
+ */
20
+ showRealDeviceFlowsInfo(options: {
21
+ realDevice: boolean;
22
+ device?: string;
23
+ version?: string;
24
+ flowCount: number;
25
+ shardSplit?: number;
26
+ }): void;
9
27
  /**
10
28
  * Check if a newer version is available and display update notice
11
29
  */
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";oBAOkB,MAAM;yBAID,MAAM;IAI3B;;;OAGG;wBACiB,MAAM,MAAM,MAAM,GAAG,MAAM;IAa/C;;OAEG;kCAC2B,MAAM,GAAG,SAAS,GAAG,IAAI;;AA7BzD,wBAuDE"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";oBAQkB,MAAM;yBAID,MAAM;IAI3B;;;OAGG;wBACiB,MAAM,MAAM,MAAM,GAAG,MAAM;IAa/C;;OAEG;6BACsB,MAAM,GAAG,SAAS,GAAG,OAAO;IAMrD;;OAEG;+BACwB,MAAM,GAAG,SAAS,GAAG,OAAO;IAMvD;;OAEG;qCAC8B;QAC/B,UAAU,EAAE,OAAO,CAAC;QACpB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,IAAI;IAuDR;;OAEG;kCAC2B,MAAM,GAAG,SAAS,GAAG,IAAI;;AA/GzD,wBAyIE"}
package/dist/utils.js CHANGED
@@ -7,6 +7,7 @@ const package_json_1 = __importDefault(require("../package.json"));
7
7
  const logger_1 = __importDefault(require("./logger"));
8
8
  const picocolors_1 = __importDefault(require("picocolors"));
9
9
  let versionCheckDisplayed = false;
10
+ let realDeviceFlowsInfoDisplayed = false;
10
11
  exports.default = {
11
12
  getUserAgent() {
12
13
  return `TestingBot-CTL-${package_json_1.default.version}`;
@@ -31,6 +32,56 @@ exports.default = {
31
32
  }
32
33
  return 0;
33
34
  },
35
+ /**
36
+ * Check if a device specification is a wildcard or regex pattern
37
+ */
38
+ isWildcardDevice(device) {
39
+ if (!device)
40
+ return true;
41
+ // Check for common wildcard/regex characters
42
+ return device === '*' || device.includes('*') || device.includes('?') || device.includes('.*');
43
+ },
44
+ /**
45
+ * Check if a version specification is a wildcard or regex pattern
46
+ */
47
+ isWildcardVersion(version) {
48
+ if (!version)
49
+ return true;
50
+ // Check for common wildcard/regex characters
51
+ return version === '*' || version.includes('*') || version.includes('?') || version.includes('.*');
52
+ },
53
+ /**
54
+ * Show info message when running many flows on a specific real device without sharding
55
+ */
56
+ showRealDeviceFlowsInfo(options) {
57
+ // Only show once
58
+ if (realDeviceFlowsInfoDisplayed) {
59
+ return;
60
+ }
61
+ // Check conditions: real device, specific device, more than 2 flows, no shards
62
+ if (!options.realDevice ||
63
+ this.isWildcardDevice(options.device) ||
64
+ options.flowCount <= 2 ||
65
+ options.shardSplit) {
66
+ return;
67
+ }
68
+ realDeviceFlowsInfoDisplayed = true;
69
+ const border = '─'.repeat(80);
70
+ logger_1.default.info('');
71
+ logger_1.default.info(picocolors_1.default.cyan(border));
72
+ logger_1.default.info(picocolors_1.default.cyan('ℹ Performance Tip'));
73
+ logger_1.default.info(picocolors_1.default.cyan(` Running ${options.flowCount} flows on a specific device (${options.device}) in real device mode.`));
74
+ logger_1.default.info(picocolors_1.default.cyan(' Each flow runs in its own session on that device, which may be slow.'));
75
+ logger_1.default.info(picocolors_1.default.cyan(''));
76
+ logger_1.default.info(picocolors_1.default.cyan(' Consider these alternatives for faster execution:'));
77
+ logger_1.default.info(picocolors_1.default.cyan(` • Use ${picocolors_1.default.white('--shard-split <n>')} to run multiple flows in the same session`));
78
+ logger_1.default.info(picocolors_1.default.cyan(` • Use wildcards for device (e.g., ${picocolors_1.default.white('"Pixel.*"')}) to parallelize across devices`));
79
+ if (!this.isWildcardVersion(options.version)) {
80
+ logger_1.default.info(picocolors_1.default.cyan(` • Use wildcards for version (e.g., ${picocolors_1.default.white('"15.*"')}) for broader device selection`));
81
+ }
82
+ logger_1.default.info(picocolors_1.default.cyan(border));
83
+ logger_1.default.info('');
84
+ },
34
85
  /**
35
86
  * Check if a newer version is available and display update notice
36
87
  */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@testingbot/cli",
3
- "version": "1.0.3",
4
- "description": "CLI tool to run Espresso, XCUITest, and Maestro tests on TestingBot's cloud infrastructure",
3
+ "version": "1.0.4",
4
+ "description": "CLI tool to run Espresso, XCUITest and Maestro tests on TestingBot's cloud infrastructure",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "testingbot": "dist/index.js"