@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.
- package/README.md +82 -6
- package/dist/logger.js +4 -4
- package/dist/models/maestro_options.d.ts +1 -0
- package/dist/models/maestro_options.d.ts.map +1 -1
- package/dist/models/maestro_options.js +5 -1
- package/dist/providers/espresso.d.ts.map +1 -1
- package/dist/providers/espresso.js +13 -13
- package/dist/providers/maestro.d.ts +28 -0
- package/dist/providers/maestro.d.ts.map +1 -1
- package/dist/providers/maestro.js +353 -77
- package/dist/providers/xcuitest.d.ts.map +1 -1
- package/dist/providers/xcuitest.js +13 -13
- package/dist/upload.d.ts +11 -0
- package/dist/upload.d.ts.map +1 -1
- package/dist/upload.js +57 -0
- package/dist/utils/connectivity.d.ts +2 -1
- package/dist/utils/connectivity.d.ts.map +1 -1
- package/dist/utils/connectivity.js +83 -70
- package/dist/utils/file-type-detector.d.ts +4 -1
- package/dist/utils/file-type-detector.d.ts.map +1 -1
- package/dist/utils/file-type-detector.js +30 -6
- package/dist/utils.d.ts +18 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +58 -7
- package/package.json +3 -3
|
@@ -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
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
+
fileChecks.push(this.ensureOutputDirectory(this.options.reportOutputDir));
|
|
94
104
|
}
|
|
95
105
|
if (this.options.downloadArtifacts && this.options.artifactsOutputDir) {
|
|
96
|
-
|
|
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
|
-
|
|
175
|
+
let appPath = this.options.app;
|
|
165
176
|
const ext = node_path_1.default.extname(appPath).toLowerCase();
|
|
166
|
-
let
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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('
|
|
183
|
+
logger_1.default.info('Zipping .app bundle for upload');
|
|
186
184
|
}
|
|
187
|
-
|
|
185
|
+
tempZipPath = await this.zipAppBundle(appPath);
|
|
186
|
+
appPath = tempZipPath;
|
|
188
187
|
}
|
|
189
188
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
384
|
+
let configPath = null;
|
|
320
385
|
let config = null;
|
|
321
|
-
// Check for config.yaml
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
|
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
|
|
355
|
-
if (
|
|
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 (
|
|
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(
|
|
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(
|
|
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:
|
|
1127
|
+
return { text: 'WAITING', colored: picocolors_1.default.white('WAITING') };
|
|
852
1128
|
case 'READY':
|
|
853
|
-
return { text: 'RUNNING', colored:
|
|
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:
|
|
1132
|
+
return { text: 'PASSED', colored: picocolors_1.default.green('PASSED') };
|
|
857
1133
|
}
|
|
858
1134
|
else {
|
|
859
|
-
return { text: 'FAILED', colored:
|
|
1135
|
+
return { text: 'FAILED', colored: picocolors_1.default.red('FAILED') };
|
|
860
1136
|
}
|
|
861
1137
|
case 'FAILED':
|
|
862
|
-
return { text: 'FAILED', colored:
|
|
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(
|
|
1206
|
+
parts.push(picocolors_1.default.white(`${waiting} waiting`));
|
|
931
1207
|
if (running > 0)
|
|
932
|
-
parts.push(
|
|
1208
|
+
parts.push(picocolors_1.default.blue(`${running} running`));
|
|
933
1209
|
if (passed > 0)
|
|
934
|
-
parts.push(
|
|
1210
|
+
parts.push(picocolors_1.default.green(`${passed} passed`));
|
|
935
1211
|
if (failed > 0)
|
|
936
|
-
parts.push(
|
|
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(
|
|
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(
|
|
963
|
-
console.log(
|
|
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 += ` ${
|
|
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} ${
|
|
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${
|
|
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;
|
|
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
|
-
|
|
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
|
}
|