@testingbot/cli 1.0.2 → 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.
@@ -48,7 +48,7 @@ const socket_io_client_1 = require("socket.io-client");
48
48
  const testingbot_error_1 = __importDefault(require("../models/testingbot_error"));
49
49
  const utils_1 = __importDefault(require("../utils"));
50
50
  const file_type_detector_1 = require("../utils/file-type-detector");
51
- const colors_1 = __importDefault(require("colors"));
51
+ const picocolors_1 = __importDefault(require("picocolors"));
52
52
  const base_provider_1 = __importDefault(require("./base_provider"));
53
53
  class Maestro extends base_provider_1.default {
54
54
  URL = 'https://api.testingbot.com/v1/app-automate/maestro';
@@ -59,42 +59,53 @@ class Maestro extends base_provider_1.default {
59
59
  constructor(credentials, options) {
60
60
  super(credentials, options);
61
61
  }
62
+ static SUPPORTED_APP_EXTENSIONS = [
63
+ '.apk',
64
+ '.apks',
65
+ '.ipa',
66
+ '.app',
67
+ '.zip',
68
+ ];
62
69
  async validate() {
63
70
  if (this.options.app === undefined) {
64
71
  throw new testingbot_error_1.default(`app option is required`);
65
72
  }
66
- try {
67
- await node_fs_1.default.promises.access(this.options.app, node_fs_1.default.constants.R_OK);
68
- }
69
- catch {
70
- throw new testingbot_error_1.default(`Provided app path does not exist ${this.options.app}`);
73
+ // Validate app file extension
74
+ const appExt = node_path_1.default.extname(this.options.app).toLowerCase();
75
+ if (!Maestro.SUPPORTED_APP_EXTENSIONS.includes(appExt)) {
76
+ throw new testingbot_error_1.default(`Unsupported app file format: ${appExt || '(no extension)'}. ` +
77
+ `Supported formats: ${Maestro.SUPPORTED_APP_EXTENSIONS.join(', ')}`);
71
78
  }
72
79
  if (this.options.flows === undefined || this.options.flows.length === 0) {
73
80
  throw new testingbot_error_1.default(`flows option is required`);
74
81
  }
82
+ if (this.options.report && !this.options.reportOutputDir) {
83
+ throw new testingbot_error_1.default(`--report-output-dir is required when --report is specified`);
84
+ }
85
+ // Build list of all file checks to run in parallel
86
+ const fileChecks = [
87
+ node_fs_1.default.promises.access(this.options.app, node_fs_1.default.constants.R_OK).catch(() => {
88
+ throw new testingbot_error_1.default(`Provided app path does not exist ${this.options.app}`);
89
+ }),
90
+ ];
75
91
  // Check if all flows paths exist (can be files, directories or glob patterns)
76
92
  for (const flowsPath of this.options.flows) {
77
93
  const isGlobPattern = flowsPath.includes('*') ||
78
94
  flowsPath.includes('?') ||
79
95
  flowsPath.includes('{');
80
96
  if (!isGlobPattern) {
81
- try {
82
- await node_fs_1.default.promises.access(flowsPath, node_fs_1.default.constants.R_OK);
83
- }
84
- catch {
97
+ fileChecks.push(node_fs_1.default.promises.access(flowsPath, node_fs_1.default.constants.R_OK).catch(() => {
85
98
  throw new testingbot_error_1.default(`flows path does not exist ${flowsPath}`);
86
- }
99
+ }));
87
100
  }
88
101
  }
89
- if (this.options.report && !this.options.reportOutputDir) {
90
- throw new testingbot_error_1.default(`--report-output-dir is required when --report is specified`);
91
- }
92
102
  if (this.options.reportOutputDir) {
93
- await this.ensureOutputDirectory(this.options.reportOutputDir);
103
+ fileChecks.push(this.ensureOutputDirectory(this.options.reportOutputDir));
94
104
  }
95
105
  if (this.options.downloadArtifacts && this.options.artifactsOutputDir) {
96
- await this.ensureOutputDirectory(this.options.artifactsOutputDir);
106
+ fileChecks.push(this.ensureOutputDirectory(this.options.artifactsOutputDir));
97
107
  }
108
+ await Promise.all(fileChecks);
98
109
  return true;
99
110
  }
100
111
  /**
@@ -161,44 +172,84 @@ class Maestro extends base_provider_1.default {
161
172
  }
162
173
  }
163
174
  async uploadApp() {
164
- const appPath = this.options.app;
175
+ let appPath = this.options.app;
165
176
  const ext = node_path_1.default.extname(appPath).toLowerCase();
166
- let contentType;
167
- if (ext === '.apk') {
168
- contentType = 'application/vnd.android.package-archive';
169
- }
170
- else if (ext === '.ipa' || ext === '.app') {
171
- contentType = 'application/octet-stream';
172
- }
173
- else if (ext === '.zip') {
174
- contentType = 'application/zip';
175
- }
176
- else {
177
- contentType = 'application/octet-stream';
178
- }
179
- if (!this.options.ignoreChecksumCheck) {
180
- const checksum = await this.upload.calculateChecksum(appPath);
181
- const existingApp = await this.checkAppChecksum(checksum);
182
- if (existingApp) {
183
- 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()) {
184
182
  if (!this.options.quiet) {
185
- logger_1.default.info(' App already uploaded, skipping upload');
183
+ logger_1.default.info('Zipping .app bundle for upload');
186
184
  }
187
- return true;
185
+ tempZipPath = await this.zipAppBundle(appPath);
186
+ appPath = tempZipPath;
188
187
  }
189
188
  }
190
- if (!this.options.quiet) {
191
- logger_1.default.info('Uploading Maestro App');
192
- }
193
- const result = await this.upload.upload({
194
- filePath: appPath,
195
- url: `${this.URL}/app`,
196
- credentials: this.credentials,
197
- contentType,
198
- 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();
199
252
  });
200
- this.appId = result.id;
201
- return true;
202
253
  }
203
254
  async checkAppChecksum(checksum) {
204
255
  try {
@@ -241,6 +292,7 @@ class Maestro extends base_provider_1.default {
241
292
  credentials: this.credentials,
242
293
  contentType: 'application/zip',
243
294
  showProgress: !this.options.quiet,
295
+ validateZipFormat: true,
244
296
  });
245
297
  return true;
246
298
  }
@@ -292,6 +344,19 @@ class Maestro extends base_provider_1.default {
292
344
  const baseDir = baseDirs.length === 1 ? baseDirs[0] : undefined;
293
345
  if (!this.options.quiet) {
294
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
+ });
355
+ }
356
+ // Check for missing file references and warn the user
357
+ const missingReferences = await this.findMissingReferences(allFlowFiles, allFlowFiles, baseDir);
358
+ if (!this.options.quiet && missingReferences.length > 0) {
359
+ this.logMissingReferences(missingReferences, baseDir);
295
360
  }
296
361
  zipPath = await this.createFlowsZip(allFlowFiles, baseDir);
297
362
  shouldCleanup = true;
@@ -316,15 +381,20 @@ class Maestro extends base_provider_1.default {
316
381
  withFileTypes: true,
317
382
  });
318
383
  const flowFiles = [];
319
- const configPath = node_path_1.default.join(directory, 'config.yaml');
384
+ let configPath = null;
320
385
  let config = null;
321
- // Check for config.yaml
322
- try {
323
- const configContent = await node_fs_1.default.promises.readFile(configPath, 'utf-8');
324
- config = yaml.load(configContent);
325
- }
326
- catch {
327
- // 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
+ }
328
398
  }
329
399
  // If config specifies flows, use those
330
400
  if (config?.flows && config.flows.length > 0) {
@@ -339,7 +409,7 @@ class Maestro extends base_provider_1.default {
339
409
  if (entry.isFile()) {
340
410
  const ext = node_path_1.default.extname(entry.name).toLowerCase();
341
411
  if ((ext === '.yaml' || ext === '.yml') &&
342
- entry.name !== 'config.yaml') {
412
+ !this.isConfigFile(entry.name)) {
343
413
  flowFiles.push(node_path_1.default.join(directory, entry.name));
344
414
  }
345
415
  }
@@ -351,8 +421,8 @@ class Maestro extends base_provider_1.default {
351
421
  const dependencies = await this.discoverDependencies(flowFile, directory);
352
422
  dependencies.forEach((dep) => allFiles.add(dep));
353
423
  }
354
- // Include config.yaml if it exists
355
- if (config) {
424
+ // Include config file if it exists
425
+ if (configPath) {
356
426
  allFiles.add(configPath);
357
427
  }
358
428
  return Array.from(allFiles);
@@ -384,6 +454,13 @@ class Maestro extends base_provider_1.default {
384
454
  }
385
455
  return dependencies;
386
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
+ }
387
464
  /**
388
465
  * Check if a string looks like a file path (relative path with extension)
389
466
  */
@@ -539,6 +616,205 @@ class Maestro extends base_provider_1.default {
539
616
  }
540
617
  return dependencies;
541
618
  }
619
+ /**
620
+ * Find all file references in flow files that don't exist on disk.
621
+ * This validates that all referenced files (runScript, runFlow, addMedia, etc.)
622
+ * will be included in the zip.
623
+ */
624
+ async findMissingReferences(flowFiles, allIncludedFiles, baseDir) {
625
+ const missingReferences = [];
626
+ const includedFilesSet = new Set(allIncludedFiles.map((f) => node_path_1.default.resolve(f)));
627
+ for (const flowFile of flowFiles) {
628
+ const ext = node_path_1.default.extname(flowFile).toLowerCase();
629
+ if (ext !== '.yaml' && ext !== '.yml') {
630
+ continue;
631
+ }
632
+ try {
633
+ const content = await node_fs_1.default.promises.readFile(flowFile, 'utf-8');
634
+ const documents = [];
635
+ yaml.loadAll(content, (doc) => documents.push(doc));
636
+ for (const flowData of documents) {
637
+ if (flowData !== null && typeof flowData === 'object') {
638
+ const missing = await this.findMissingInValue(flowData, flowFile, includedFilesSet);
639
+ missingReferences.push(...missing);
640
+ }
641
+ }
642
+ }
643
+ catch {
644
+ // Ignore parsing errors
645
+ }
646
+ }
647
+ return missingReferences;
648
+ }
649
+ /**
650
+ * Recursively find missing file references in a YAML value
651
+ */
652
+ async findMissingInValue(value, flowFile, includedFiles) {
653
+ const missingReferences = [];
654
+ if (typeof value === 'string') {
655
+ if (this.looksLikePath(value)) {
656
+ const resolvedPath = node_path_1.default.resolve(node_path_1.default.dirname(flowFile), value);
657
+ // Check if the file is in included files OR exists on disk
658
+ if (!includedFiles.has(resolvedPath)) {
659
+ try {
660
+ await node_fs_1.default.promises.access(resolvedPath);
661
+ // File exists on disk but won't be included - also warn
662
+ }
663
+ catch {
664
+ // File doesn't exist
665
+ missingReferences.push({
666
+ flowFile,
667
+ referencedFile: value,
668
+ resolvedPath,
669
+ });
670
+ }
671
+ }
672
+ }
673
+ }
674
+ else if (Array.isArray(value)) {
675
+ for (const item of value) {
676
+ const missing = await this.findMissingInValue(item, flowFile, includedFiles);
677
+ missingReferences.push(...missing);
678
+ }
679
+ }
680
+ else if (value !== null && typeof value === 'object') {
681
+ const obj = value;
682
+ const handledKeys = new Set();
683
+ // Handle runScript - extract file reference but don't recurse
684
+ // (runScript objects only contain file, env, when - no nested file refs)
685
+ if ('runScript' in obj) {
686
+ handledKeys.add('runScript');
687
+ const runScript = obj.runScript;
688
+ const scriptFile = typeof runScript === 'string'
689
+ ? runScript
690
+ : runScript?.file;
691
+ if (typeof scriptFile === 'string') {
692
+ const resolved = node_path_1.default.resolve(node_path_1.default.dirname(flowFile), scriptFile);
693
+ if (!includedFiles.has(resolved)) {
694
+ try {
695
+ await node_fs_1.default.promises.access(resolved);
696
+ }
697
+ catch {
698
+ missingReferences.push({
699
+ flowFile,
700
+ referencedFile: scriptFile,
701
+ resolvedPath: resolved,
702
+ });
703
+ }
704
+ }
705
+ }
706
+ // Don't recurse into runScript - it only has file, env, when (no nested file refs)
707
+ }
708
+ // Handle runFlow - extract file reference and recurse only into commands
709
+ if ('runFlow' in obj) {
710
+ handledKeys.add('runFlow');
711
+ const runFlow = obj.runFlow;
712
+ const flowRef = typeof runFlow === 'string'
713
+ ? runFlow
714
+ : runFlow?.file;
715
+ if (typeof flowRef === 'string') {
716
+ const resolved = node_path_1.default.resolve(node_path_1.default.dirname(flowFile), flowRef);
717
+ if (!includedFiles.has(resolved)) {
718
+ try {
719
+ await node_fs_1.default.promises.access(resolved);
720
+ }
721
+ catch {
722
+ missingReferences.push({
723
+ flowFile,
724
+ referencedFile: flowRef,
725
+ resolvedPath: resolved,
726
+ });
727
+ }
728
+ }
729
+ }
730
+ // Only recurse into 'commands' if present (for inline commands)
731
+ if (typeof runFlow === 'object' &&
732
+ runFlow !== null &&
733
+ 'commands' in runFlow) {
734
+ const commands = runFlow.commands;
735
+ if (Array.isArray(commands)) {
736
+ const nestedMissing = await this.findMissingInValue(commands, flowFile, includedFiles);
737
+ missingReferences.push(...nestedMissing);
738
+ }
739
+ }
740
+ }
741
+ // Handle addMedia
742
+ if ('addMedia' in obj) {
743
+ handledKeys.add('addMedia');
744
+ const addMedia = obj.addMedia;
745
+ const mediaFiles = Array.isArray(addMedia) ? addMedia : [addMedia];
746
+ for (const mediaFile of mediaFiles) {
747
+ if (typeof mediaFile === 'string') {
748
+ const resolved = node_path_1.default.resolve(node_path_1.default.dirname(flowFile), mediaFile);
749
+ if (!includedFiles.has(resolved)) {
750
+ try {
751
+ await node_fs_1.default.promises.access(resolved);
752
+ }
753
+ catch {
754
+ missingReferences.push({
755
+ flowFile,
756
+ referencedFile: mediaFile,
757
+ resolvedPath: resolved,
758
+ });
759
+ }
760
+ }
761
+ }
762
+ }
763
+ }
764
+ // Handle file property
765
+ if ('file' in obj && typeof obj.file === 'string') {
766
+ handledKeys.add('file');
767
+ const resolved = node_path_1.default.resolve(node_path_1.default.dirname(flowFile), obj.file);
768
+ if (!includedFiles.has(resolved)) {
769
+ try {
770
+ await node_fs_1.default.promises.access(resolved);
771
+ }
772
+ catch {
773
+ missingReferences.push({
774
+ flowFile,
775
+ referencedFile: obj.file,
776
+ resolvedPath: resolved,
777
+ });
778
+ }
779
+ }
780
+ }
781
+ // Handle onFlowStart, onFlowComplete, commands
782
+ for (const key of ['onFlowStart', 'onFlowComplete', 'commands']) {
783
+ if (key in obj) {
784
+ handledKeys.add(key);
785
+ const nested = obj[key];
786
+ if (Array.isArray(nested)) {
787
+ const nestedMissing = await this.findMissingInValue(nested, flowFile, includedFiles);
788
+ missingReferences.push(...nestedMissing);
789
+ }
790
+ }
791
+ }
792
+ // Recursively check remaining properties
793
+ for (const [key, propValue] of Object.entries(obj)) {
794
+ if (!handledKeys.has(key)) {
795
+ const nestedMissing = await this.findMissingInValue(propValue, flowFile, includedFiles);
796
+ missingReferences.push(...nestedMissing);
797
+ }
798
+ }
799
+ }
800
+ return missingReferences;
801
+ }
802
+ /**
803
+ * Log warnings for missing file references
804
+ */
805
+ logMissingReferences(missingReferences, baseDir) {
806
+ if (missingReferences.length === 0) {
807
+ return;
808
+ }
809
+ logger_1.default.warn(`Warning: ${missingReferences.length} referenced file(s) not found:`);
810
+ for (const ref of missingReferences) {
811
+ const flowRelative = baseDir
812
+ ? node_path_1.default.relative(baseDir, ref.flowFile)
813
+ : node_path_1.default.basename(ref.flowFile);
814
+ logger_1.default.warn(` In ${flowRelative}: ${ref.referencedFile}`);
815
+ }
816
+ logger_1.default.warn('These files will not be included in the upload and may cause test failures.');
817
+ }
542
818
  logIncludedFiles(files, baseDir) {
543
819
  // Get relative paths for display
544
820
  const relativePaths = files
@@ -555,7 +831,7 @@ class Maestro extends base_provider_1.default {
555
831
  for (const filePath of relativePaths) {
556
832
  const ext = node_path_1.default.extname(filePath).toLowerCase();
557
833
  if (ext === '.yaml' || ext === '.yml') {
558
- if (filePath === 'config.yaml' || filePath.endsWith('/config.yaml')) {
834
+ if (this.isConfigFile(filePath)) {
559
835
  groups['Config files'].push(filePath);
560
836
  }
561
837
  else {
@@ -753,9 +1029,9 @@ class Maestro extends base_provider_1.default {
753
1029
  process.stdout.write(`\x1b[${linesToMove}A`);
754
1030
  // Clear header line, write new header, then clear separator line
755
1031
  process.stdout.write('\x1b[2K');
756
- console.log(colors_1.default.dim(` ${'Duration'.padEnd(10)} ${'Status'.padEnd(8)} Flow Fail reason`));
1032
+ console.log(picocolors_1.default.dim(` ${'Duration'.padEnd(10)} ${'Status'.padEnd(8)} Flow Fail reason`));
757
1033
  process.stdout.write('\x1b[2K');
758
- console.log(colors_1.default.dim(` ${'─'.repeat(10)} ${'─'.repeat(8)} ${'─'.repeat(30)} ${'─'.repeat(80)}`));
1034
+ console.log(picocolors_1.default.dim(` ${'─'.repeat(10)} ${'─'.repeat(8)} ${'─'.repeat(30)} ${'─'.repeat(80)}`));
759
1035
  // Redraw all flows with error messages
760
1036
  for (const flow of allFlows) {
761
1037
  // Clear the line before writing
@@ -848,18 +1124,18 @@ class Maestro extends base_provider_1.default {
848
1124
  getFlowStatusDisplay(flow) {
849
1125
  switch (flow.status) {
850
1126
  case 'WAITING':
851
- return { text: 'WAITING', colored: colors_1.default.white('WAITING') };
1127
+ return { text: 'WAITING', colored: picocolors_1.default.white('WAITING') };
852
1128
  case 'READY':
853
- return { text: 'RUNNING', colored: colors_1.default.blue('RUNNING') };
1129
+ return { text: 'RUNNING', colored: picocolors_1.default.blue('RUNNING') };
854
1130
  case 'DONE':
855
1131
  if (flow.success === 1) {
856
- return { text: 'PASSED', colored: colors_1.default.green('PASSED') };
1132
+ return { text: 'PASSED', colored: picocolors_1.default.green('PASSED') };
857
1133
  }
858
1134
  else {
859
- return { text: 'FAILED', colored: colors_1.default.red('FAILED') };
1135
+ return { text: 'FAILED', colored: picocolors_1.default.red('FAILED') };
860
1136
  }
861
1137
  case 'FAILED':
862
- return { text: 'FAILED', colored: colors_1.default.red('FAILED') };
1138
+ return { text: 'FAILED', colored: picocolors_1.default.red('FAILED') };
863
1139
  default:
864
1140
  return { text: flow.status, colored: flow.status };
865
1141
  }
@@ -927,13 +1203,13 @@ class Maestro extends base_provider_1.default {
927
1203
  }
928
1204
  const parts = [];
929
1205
  if (waiting > 0)
930
- parts.push(colors_1.default.white(`${waiting} waiting`));
1206
+ parts.push(picocolors_1.default.white(`${waiting} waiting`));
931
1207
  if (running > 0)
932
- parts.push(colors_1.default.blue(`${running} running`));
1208
+ parts.push(picocolors_1.default.blue(`${running} running`));
933
1209
  if (passed > 0)
934
- parts.push(colors_1.default.green(`${passed} passed`));
1210
+ parts.push(picocolors_1.default.green(`${passed} passed`));
935
1211
  if (failed > 0)
936
- parts.push(colors_1.default.red(`${failed} failed`));
1212
+ parts.push(picocolors_1.default.red(`${failed} failed`));
937
1213
  return ` ... and ${remaining.length} more: ${parts.join(', ')}`;
938
1214
  }
939
1215
  displayFlowsWithLimit(flows, previousFlowStatus, hasFailures = false) {
@@ -947,7 +1223,7 @@ class Maestro extends base_provider_1.default {
947
1223
  // Show summary for remaining flows
948
1224
  if (flows.length > maxFlows) {
949
1225
  const summary = this.getRemainingSummary(flows, maxFlows);
950
- console.log(colors_1.default.dim(summary));
1226
+ console.log(picocolors_1.default.dim(summary));
951
1227
  linesWritten++;
952
1228
  }
953
1229
  return linesWritten;
@@ -959,8 +1235,8 @@ class Maestro extends base_provider_1.default {
959
1235
  header += ' Fail reason';
960
1236
  separator += ` ${'─'.repeat(80)}`;
961
1237
  }
962
- console.log(colors_1.default.dim(header));
963
- console.log(colors_1.default.dim(separator));
1238
+ console.log(picocolors_1.default.dim(header));
1239
+ console.log(picocolors_1.default.dim(separator));
964
1240
  }
965
1241
  displayFlowRow(flow, isUpdate = false, hasFailures = false) {
966
1242
  const duration = this.calculateFlowDuration(flow).padEnd(10);
@@ -976,7 +1252,7 @@ class Maestro extends base_provider_1.default {
976
1252
  let row = ` ${duration} ${statusPadded} ${name}`;
977
1253
  // Add first error message on the same line if failed and has errors
978
1254
  if (hasFailures && isFailed && errorMessages.length > 0) {
979
- row += ` ${colors_1.default.red(errorMessages[0])}`;
1255
+ row += ` ${picocolors_1.default.red(errorMessages[0])}`;
980
1256
  }
981
1257
  if (isUpdate) {
982
1258
  process.stdout.write(`\r${row}`);
@@ -990,7 +1266,7 @@ class Maestro extends base_provider_1.default {
990
1266
  // Indent to align with the Fail reason column: Duration(11) + Status(9) + Test(31) = 51 chars
991
1267
  const indent = ' '.repeat(51);
992
1268
  for (let i = 1; i < errorMessages.length; i++) {
993
- console.log(`${indent} ${colors_1.default.red(errorMessages[i])}`);
1269
+ console.log(`${indent} ${picocolors_1.default.red(errorMessages[i])}`);
994
1270
  linesWritten++;
995
1271
  }
996
1272
  }
@@ -1035,7 +1311,7 @@ class Maestro extends base_provider_1.default {
1035
1311
  // Update or add summary line for remaining flows
1036
1312
  if (hasRemaining) {
1037
1313
  const summary = this.getRemainingSummary(flows, maxFlows);
1038
- process.stdout.write(`\r\x1b[K${colors_1.default.dim(summary)}\n`);
1314
+ process.stdout.write(`\r\x1b[K${picocolors_1.default.dim(summary)}\n`);
1039
1315
  linesWritten++;
1040
1316
  }
1041
1317
  // Return the number of lines we wrote
@@ -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"}
@@ -23,28 +23,26 @@ class XCUITest extends base_provider_1.default {
23
23
  if (this.options.app === undefined) {
24
24
  throw new testingbot_error_1.default(`app option is required`);
25
25
  }
26
- try {
27
- await node_fs_1.default.promises.access(this.options.app, node_fs_1.default.constants.R_OK);
28
- }
29
- catch {
30
- throw new testingbot_error_1.default(`Provided app path does not exist ${this.options.app}`);
31
- }
32
26
  if (this.options.testApp === undefined) {
33
27
  throw new testingbot_error_1.default(`testApp option is required`);
34
28
  }
35
- try {
36
- await node_fs_1.default.promises.access(this.options.testApp, node_fs_1.default.constants.R_OK);
37
- }
38
- catch {
39
- throw new testingbot_error_1.default(`testApp path does not exist ${this.options.testApp}`);
40
- }
41
29
  // Validate report options
42
30
  if (this.options.report && !this.options.reportOutputDir) {
43
31
  throw new testingbot_error_1.default(`--report-output-dir is required when --report is specified`);
44
32
  }
33
+ // Validate file access in parallel for better performance
34
+ const fileChecks = [
35
+ node_fs_1.default.promises.access(this.options.app, node_fs_1.default.constants.R_OK).catch(() => {
36
+ throw new testingbot_error_1.default(`Provided app path does not exist ${this.options.app}`);
37
+ }),
38
+ node_fs_1.default.promises.access(this.options.testApp, node_fs_1.default.constants.R_OK).catch(() => {
39
+ throw new testingbot_error_1.default(`testApp path does not exist ${this.options.testApp}`);
40
+ }),
41
+ ];
45
42
  if (this.options.reportOutputDir) {
46
- await this.ensureOutputDirectory(this.options.reportOutputDir);
43
+ fileChecks.push(this.ensureOutputDirectory(this.options.reportOutputDir));
47
44
  }
45
+ await Promise.all(fileChecks);
48
46
  return true;
49
47
  }
50
48
  async run() {
@@ -106,6 +104,7 @@ class XCUITest extends base_provider_1.default {
106
104
  credentials: this.credentials,
107
105
  contentType: 'application/octet-stream',
108
106
  showProgress: !this.options.quiet,
107
+ validateZipFormat: true,
109
108
  });
110
109
  this.appId = result.id;
111
110
  return true;
@@ -117,6 +116,7 @@ class XCUITest extends base_provider_1.default {
117
116
  credentials: this.credentials,
118
117
  contentType: 'application/zip',
119
118
  showProgress: !this.options.quiet,
119
+ validateZipFormat: true,
120
120
  });
121
121
  return true;
122
122
  }