@devicecloud.dev/dcd 4.1.2 → 4.1.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.
Files changed (47) hide show
  1. package/dist/commands/cloud.d.ts +26 -34
  2. package/dist/commands/cloud.js +151 -513
  3. package/dist/config/flags/api.flags.d.ts +7 -0
  4. package/dist/config/flags/api.flags.js +19 -0
  5. package/dist/config/flags/binary.flags.d.ts +8 -0
  6. package/dist/config/flags/binary.flags.js +20 -0
  7. package/dist/config/flags/device.flags.d.ts +14 -0
  8. package/dist/config/flags/device.flags.js +46 -0
  9. package/dist/config/flags/environment.flags.d.ts +11 -0
  10. package/dist/config/flags/environment.flags.js +37 -0
  11. package/dist/config/flags/execution.flags.d.ts +13 -0
  12. package/dist/config/flags/execution.flags.js +50 -0
  13. package/dist/config/flags/output.flags.d.ts +18 -0
  14. package/dist/config/flags/output.flags.js +61 -0
  15. package/dist/constants.d.ts +28 -24
  16. package/dist/constants.js +21 -206
  17. package/dist/gateways/api-gateway.d.ts +3 -3
  18. package/dist/gateways/api-gateway.js +1 -1
  19. package/dist/methods.d.ts +0 -4
  20. package/dist/methods.js +15 -80
  21. package/dist/services/device-validation.service.d.ts +31 -0
  22. package/dist/services/device-validation.service.js +74 -0
  23. package/dist/{plan.d.ts → services/execution-plan.service.d.ts} +10 -2
  24. package/dist/{plan.js → services/execution-plan.service.js} +12 -11
  25. package/dist/{planMethods.js → services/execution-plan.utils.js} +0 -1
  26. package/dist/services/metadata-extractor.service.d.ts +46 -0
  27. package/dist/services/metadata-extractor.service.js +138 -0
  28. package/dist/services/moropo.service.d.ts +25 -0
  29. package/dist/services/moropo.service.js +118 -0
  30. package/dist/services/report-download.service.d.ts +43 -0
  31. package/dist/services/report-download.service.js +113 -0
  32. package/dist/services/results-polling.service.d.ts +51 -0
  33. package/dist/services/results-polling.service.js +224 -0
  34. package/dist/services/test-submission.service.d.ts +47 -0
  35. package/dist/services/test-submission.service.js +122 -0
  36. package/dist/services/version.service.d.ts +32 -0
  37. package/dist/services/version.service.js +82 -0
  38. package/dist/types/{schema.types.d.ts → generated/schema.types.d.ts} +349 -349
  39. package/dist/types/index.d.ts +6 -0
  40. package/dist/types/index.js +24 -0
  41. package/dist/utils/compatibility.d.ts +5 -0
  42. package/oclif.manifest.json +195 -209
  43. package/package.json +2 -9
  44. /package/dist/{planMethods.d.ts → services/execution-plan.utils.d.ts} +0 -0
  45. /package/dist/types/{device.types.d.ts → domain/device.types.d.ts} +0 -0
  46. /package/dist/types/{device.types.js → domain/device.types.js} +0 -0
  47. /package/dist/types/{schema.types.js → generated/schema.types.js} +0 -0
@@ -1,26 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.mimeTypeLookupByExtension = void 0;
4
3
  /* eslint-disable complexity */
5
4
  const core_1 = require("@oclif/core");
6
- const cli_ux_1 = require("@oclif/core/lib/cli-ux");
7
5
  const errors_1 = require("@oclif/core/lib/errors");
8
- const node_child_process_1 = require("node:child_process");
9
- const fs = require("node:fs");
10
- const os = require("node:os");
11
6
  const path = require("node:path");
12
7
  const constants_1 = require("../constants");
13
8
  const api_gateway_1 = require("../gateways/api-gateway");
14
9
  const methods_1 = require("../methods");
15
- const plan_1 = require("../plan");
10
+ const device_validation_service_1 = require("../services/device-validation.service");
11
+ const execution_plan_service_1 = require("../services/execution-plan.service");
12
+ const moropo_service_1 = require("../services/moropo.service");
13
+ const report_download_service_1 = require("../services/report-download.service");
14
+ const results_polling_service_1 = require("../services/results-polling.service");
15
+ const test_submission_service_1 = require("../services/test-submission.service");
16
+ const version_service_1 = require("../services/version.service");
16
17
  const compatibility_1 = require("../utils/compatibility");
17
- const connectivity_1 = require("../utils/connectivity");
18
- const StreamZip = require("node-stream-zip");
19
- exports.mimeTypeLookupByExtension = {
20
- apk: 'application/vnd.android.package-archive',
21
- yaml: 'application/x-yaml',
22
- zip: 'application/zip',
23
- };
24
18
  // Suppress punycode deprecation warning (caused by whatwg, supabase dependancy)
25
19
  process.removeAllListeners('warning');
26
20
  process.on('warning', (warning) => {
@@ -46,25 +40,24 @@ class Cloud extends core_1.Command {
46
40
  static enableJsonFlag = true;
47
41
  static examples = ['<%= config.bin %> <%= command.id %>'];
48
42
  static flags = constants_1.flags;
43
+ deviceValidationService = new device_validation_service_1.DeviceValidationService();
44
+ moropoService = new moropo_service_1.MoropoService();
45
+ reportDownloadService = new report_download_service_1.ReportDownloadService();
46
+ resultsPollingService = new results_polling_service_1.ResultsPollingService();
47
+ testSubmissionService = new test_submission_service_1.TestSubmissionService();
49
48
  versionCheck = async () => {
50
- try {
51
- const latestVersion = (0, node_child_process_1.execSync)('npm view @devicecloud.dev/dcd version', {
52
- encoding: 'utf8',
53
- stdio: ['ignore', 'pipe', 'ignore'],
54
- }).trim();
55
- if (latestVersion !== this.config.version) {
56
- this.log(`
49
+ const latestVersion = await this.versionService.checkLatestCliVersion();
50
+ if (latestVersion &&
51
+ this.versionService.isOutdated(this.config.version, latestVersion)) {
52
+ this.log(`
57
53
  -------------------
58
54
  A new version of the devicecloud.dev CLI is available: ${latestVersion}
59
55
  Run 'npm install -g @devicecloud.dev/dcd@latest' to update to the latest version
60
56
  -------------------
61
57
  `);
62
- }
63
- }
64
- catch {
65
- // Silently fail if npm view command fails (e.g., no network connection)
66
58
  }
67
59
  };
60
+ versionService = new version_service_1.VersionService();
68
61
  async run() {
69
62
  let output = null;
70
63
  // Store debug flag outside try/catch to access it in catch block
@@ -72,9 +65,7 @@ class Cloud extends core_1.Command {
72
65
  let jsonFile = false;
73
66
  try {
74
67
  const { args, flags, raw } = await this.parse(Cloud);
75
- let { 'android-api-level': androidApiLevel, 'android-device': androidDevice, apiKey: apiKeyFlag, apiUrl, 'app-binary-id': appBinaryId, 'app-file': appFile, 'artifacts-path': artifactsPath, 'junit-path': junitPath, 'allure-path': allurePath, 'html-path': htmlPath, async, config: configFile, debug, 'device-locale': deviceLocale, 'download-artifacts': downloadArtifacts, 'dry-run': dryRun, env, 'exclude-flows': excludeFlows, 'exclude-tags': excludeTags, flows, 'google-play': googlePlay, 'ignore-sha-check': ignoreShaCheck, 'include-tags': includeTags, 'ios-device': iOSDevice, 'ios-version': iOSVersion, json, 'json-file-name': jsonFileName, 'maestro-version': maestroVersion, metadata, mitmHost, mitmPath, 'moropo-v1-api-key': moropoApiKey, name, orientation, quiet, report, retry, 'runner-type': runnerType, ...rest } = flags;
76
- // Resolve "latest" maestro version to actual version
77
- const resolvedMaestroVersion = (0, constants_1.resolveMaestroVersion)(maestroVersion);
68
+ let { 'android-api-level': androidApiLevel, 'android-device': androidDevice, apiKey: apiKeyFlag, apiUrl, 'app-binary-id': appBinaryId, 'app-file': appFile, 'artifacts-path': artifactsPath, 'junit-path': junitPath, 'allure-path': allurePath, 'html-path': htmlPath, async, config: configFile, debug, 'device-locale': deviceLocale, 'download-artifacts': downloadArtifacts, 'dry-run': dryRun, env, 'exclude-flows': excludeFlows, 'exclude-tags': excludeTags, flows, 'google-play': googlePlay, 'ignore-sha-check': ignoreShaCheck, 'include-tags': includeTags, 'ios-device': iOSDevice, 'ios-version': iOSVersion, json, 'json-file-name': jsonFileName, 'maestro-version': maestroVersion, metadata, mitmHost, mitmPath, 'moropo-v1-api-key': moropoApiKey, name, orientation, quiet, report, retry, 'runner-type': runnerType, } = flags;
78
69
  // Store debug flag for use in catch block
79
70
  debugFlag = debug === true;
80
71
  jsonFile = flags['json-file'] === true;
@@ -108,99 +99,14 @@ class Cloud extends core_1.Command {
108
99
  await this.versionCheck();
109
100
  // Download and expand Moropo zip if API key is present
110
101
  if (moropoApiKey) {
111
- if (debug) {
112
- this.log('DEBUG: Moropo v1 API key detected, downloading tests from Moropo API');
113
- }
114
- if (debug) {
115
- this.log(`DEBUG: Using branch name: main`);
116
- }
117
- try {
118
- if (!quiet && !json) {
119
- core_1.ux.action.start('Downloading Moropo tests', 'Initializing', {
120
- stdout: true,
121
- });
122
- }
123
- const response = await fetch('https://api.moropo.com/tests', {
124
- headers: {
125
- accept: 'application/zip',
126
- 'x-app-api-key': moropoApiKey,
127
- 'x-branch-name': 'main',
128
- },
129
- });
130
- if (!response.ok) {
131
- throw new Error(`Failed to download Moropo tests: ${response.statusText}`);
132
- }
133
- const contentLength = response.headers.get('content-length');
134
- const totalSize = contentLength
135
- ? Number.parseInt(contentLength, 10)
136
- : 0;
137
- let downloadedSize = 0;
138
- const moropoDir = path.join(os.tmpdir(), `moropo-tests-${Date.now()}`);
139
- if (debug) {
140
- this.log(`DEBUG: Extracting Moropo tests to: ${moropoDir}`);
141
- }
142
- // Create moropo directory if it doesn't exist
143
- if (!fs.existsSync(moropoDir)) {
144
- fs.mkdirSync(moropoDir, { recursive: true });
145
- }
146
- // Write zip file to moropo directory
147
- const zipPath = path.join(moropoDir, 'moropo-tests.zip');
148
- const fileStream = fs.createWriteStream(zipPath);
149
- const reader = response.body?.getReader();
150
- if (!reader) {
151
- throw new Error('Failed to get response reader');
152
- }
153
- let readerResult = await reader.read();
154
- while (!readerResult.done) {
155
- const { value } = readerResult;
156
- downloadedSize += value.length;
157
- if (!quiet && !json && totalSize) {
158
- const progress = Math.round((downloadedSize / totalSize) * 100);
159
- core_1.ux.action.status = `Downloading: ${progress}%`;
160
- }
161
- fileStream.write(value);
162
- readerResult = await reader.read();
163
- }
164
- fileStream.end();
165
- await new Promise((resolve) => {
166
- fileStream.on('finish', () => {
167
- resolve();
168
- });
169
- });
170
- if (!quiet && !json) {
171
- core_1.ux.action.status = 'Extracting tests...';
172
- }
173
- // Extract zip file
174
- // eslint-disable-next-line new-cap
175
- const zip = new StreamZip.async({ file: zipPath });
176
- await zip.extract(null, moropoDir);
177
- await zip.close();
178
- // Delete zip file after extraction
179
- fs.unlinkSync(zipPath);
180
- if (!quiet && !json) {
181
- core_1.ux.action.stop('completed');
182
- }
183
- if (debug) {
184
- this.log('DEBUG: Successfully extracted Moropo tests');
185
- }
186
- // Create config.yaml file
187
- const configPath = path.join(moropoDir, 'config.yaml');
188
- fs.writeFileSync(configPath, 'flows:\n- ./**/*.yaml\n- ./*.yaml\n');
189
- if (debug) {
190
- this.log('DEBUG: Created config.yaml file');
191
- }
192
- // Update flows to point to the extracted directory
193
- flows = moropoDir;
194
- }
195
- catch (error) {
196
- if (!quiet && !json) {
197
- core_1.ux.action.stop('failed');
198
- }
199
- if (debug) {
200
- this.log(`DEBUG: Error downloading/extracting Moropo tests: ${error}`);
201
- }
202
- throw new Error(`Failed to download/extract Moropo tests: ${error}`);
203
- }
102
+ flows = await this.moropoService.downloadAndExtract({
103
+ apiKey: moropoApiKey,
104
+ branchName: 'main',
105
+ debug,
106
+ json,
107
+ logger: this.log.bind(this),
108
+ quiet,
109
+ });
204
110
  }
205
111
  const apiKey = apiKeyFlag || process.env.DEVICE_CLOUD_API_KEY;
206
112
  if (!apiKey)
@@ -223,6 +129,11 @@ class Cloud extends core_1.Command {
223
129
  if (debug) {
224
130
  this.log(`DEBUG: API URL: ${apiUrl}`);
225
131
  }
132
+ // Resolve and validate Maestro version using API data
133
+ const resolvedMaestroVersion = this.versionService.resolveMaestroVersion(maestroVersion, compatibilityData, {
134
+ debug,
135
+ logger: this.log.bind(this),
136
+ });
226
137
  if (retry && retry > 2) {
227
138
  this.log("Retries are now free of charge but limited to 2. If you're test is still failing after 2 retries, please ask for help on Discord.");
228
139
  flags.retry = 2;
@@ -257,44 +168,10 @@ class Cloud extends core_1.Command {
257
168
  if (!flowFile) {
258
169
  throw new Error('You must provide a flow file');
259
170
  }
260
- if (iOSVersion || iOSDevice) {
261
- const iOSDeviceID = iOSDevice || 'iphone-14';
262
- const supportediOSVersions = compatibilityData?.ios?.[iOSDeviceID] || [];
263
- const version = iOSVersion || '17';
264
- if (supportediOSVersions.length === 0) {
265
- throw new Error(`Device ${iOSDeviceID} is not supported. Please check the docs for supported devices: https://docs.devicecloud.dev/getting-started/devices-configuration`);
266
- }
267
- if (Array.isArray(supportediOSVersions) &&
268
- !supportediOSVersions.includes(version)) {
269
- throw new Error(`${iOSDeviceID} only supports these iOS versions: ${supportediOSVersions.join(', ')}`);
270
- }
271
- if (debug) {
272
- this.log(`DEBUG: iOS device: ${iOSDeviceID}`);
273
- this.log(`DEBUG: iOS version: ${version}`);
274
- this.log(`DEBUG: Supported iOS versions: ${supportediOSVersions.join(', ')}`);
275
- }
276
- }
277
- if (androidApiLevel || androidDevice) {
278
- const androidDeviceID = androidDevice || 'pixel-7';
279
- const lookup = googlePlay
280
- ? compatibilityData.androidPlay
281
- : compatibilityData.android;
282
- const supportedAndroidVersions = lookup?.[androidDeviceID] || [];
283
- const version = androidApiLevel || '34';
284
- if (supportedAndroidVersions.length === 0) {
285
- throw new Error(`We don't support that device configuration - please check the docs for supported devices: https://docs.devicecloud.dev/getting-started/devices-configuration`);
286
- }
287
- if (Array.isArray(supportedAndroidVersions) &&
288
- !supportedAndroidVersions.includes(version)) {
289
- throw new Error(`${androidDeviceID} ${googlePlay ? '(Play Store) ' : ''}only supports these Android API levels: ${supportedAndroidVersions.join(', ')}`);
290
- }
291
- if (debug) {
292
- this.log(`DEBUG: Android device: ${androidDeviceID}`);
293
- this.log(`DEBUG: Android API level: ${version}`);
294
- this.log(`DEBUG: Google Play enabled: ${googlePlay}`);
295
- this.log(`DEBUG: Supported Android versions: ${supportedAndroidVersions.join(', ')}`);
296
- }
297
- }
171
+ // Validate iOS device configuration
172
+ this.deviceValidationService.validateiOSDevice(iOSVersion, iOSDevice, compatibilityData, { debug, logger: this.log.bind(this) });
173
+ // Validate Android device configuration
174
+ this.deviceValidationService.validateAndroidDevice(androidApiLevel, androidDevice, googlePlay, compatibilityData, { debug, logger: this.log.bind(this) });
298
175
  flowFile = path.resolve(flowFile);
299
176
  if (!flowFile?.endsWith('.yaml') &&
300
177
  !flowFile?.endsWith('.yml') &&
@@ -309,7 +186,14 @@ class Cloud extends core_1.Command {
309
186
  if (debug) {
310
187
  this.log('DEBUG: Generating execution plan...');
311
188
  }
312
- executionPlan = await (0, plan_1.plan)(flowFile, includeTags.flat(), excludeTags.flat(), excludeFlows.flat(), configFile, debug);
189
+ executionPlan = await (0, execution_plan_service_1.plan)({
190
+ input: flowFile,
191
+ includeTags: includeTags.flat(),
192
+ excludeTags: excludeTags.flat(),
193
+ excludeFlows: excludeFlows.flat(),
194
+ configFile,
195
+ debug,
196
+ });
313
197
  if (debug) {
314
198
  this.log(`DEBUG: Execution plan generated`);
315
199
  this.log(`DEBUG: Total flow files: ${executionPlan.totalFlowFiles}`);
@@ -324,7 +208,7 @@ class Cloud extends core_1.Command {
324
208
  }
325
209
  throw error;
326
210
  }
327
- const { allExcludeTags, allIncludeTags, flowMetadata, flowOverrides, flowsToRun: testFileNames, referencedFiles, sequence, workspaceConfig, } = executionPlan;
211
+ const { allExcludeTags, allIncludeTags, flowOverrides, flowsToRun: testFileNames, referencedFiles, sequence, } = executionPlan;
328
212
  if (debug) {
329
213
  this.log(`DEBUG: All include tags: ${allIncludeTags?.join(', ') || 'none'}`);
330
214
  this.log(`DEBUG: All exclude tags: ${allExcludeTags?.join(', ') || 'none'}`);
@@ -428,106 +312,39 @@ class Cloud extends core_1.Command {
428
312
  this.log(`DEBUG: Binary uploaded with ID: ${binaryId}`);
429
313
  }
430
314
  }
431
- const testFormData = new FormData();
432
- // eslint-disable-next-line unicorn/no-array-reduce
433
- const envObject = (env ?? []).reduce((acc, cur) => {
434
- const [key, ...value] = cur.split('=');
435
- // handle case where value includes an equals sign
436
- acc[key] = value.join('=');
437
- return acc;
438
- }, {});
439
- // eslint-disable-next-line unicorn/no-array-reduce
440
- const metadataObject = (metadata ?? []).reduce((acc, cur) => {
441
- const [key, ...value] = cur.split('=');
442
- // handle case where value includes an equals sign
443
- acc[key] = value.join('=');
444
- return acc;
445
- }, {});
446
- if (debug && Object.keys(envObject).length > 0) {
447
- this.log(`DEBUG: Environment variables: ${JSON.stringify(envObject)}`);
448
- }
449
- if (debug && Object.keys(metadataObject).length > 0) {
450
- this.log(`DEBUG: User metadata: ${JSON.stringify(metadataObject)}`);
451
- }
452
- if (debug) {
453
- this.log(`DEBUG: Compressing files from path: ${flowFile}`);
454
- }
455
- const buffer = await (0, methods_1.compressFilesFromRelativePath)(flowFile?.endsWith('.yaml') || flowFile?.endsWith('.yml')
456
- ? path.dirname(flowFile)
457
- : flowFile, [
458
- ...new Set([
459
- ...referencedFiles,
460
- ...testFileNames,
461
- ...sequentialFlows,
462
- ]),
463
- ], commonRoot);
464
- if (debug) {
465
- this.log(`DEBUG: Compressed file size: ${buffer.length} bytes`);
466
- }
467
- const blob = new Blob([buffer], {
468
- type: exports.mimeTypeLookupByExtension.zip,
469
- });
470
- testFormData.set('file', blob, 'flowFile.zip');
471
315
  // finalBinaryId should always be defined after validation - fail fast if not
472
316
  if (!finalBinaryId) {
473
317
  throw new Error('Internal error: finalBinaryId should be defined after validation');
474
318
  }
475
- testFormData.set('appBinaryId', finalBinaryId);
476
- testFormData.set('testFileNames', JSON.stringify(testFileNames.map((t) => t.replaceAll(commonRoot, '.').split(path.sep).join('/'))));
477
- testFormData.set('flowMetadata', JSON.stringify(Object.fromEntries(Object.entries(flowMetadata).map(([key, value]) => [
478
- key.replaceAll(commonRoot, '.').split(path.sep).join('/'),
479
- value,
480
- ]))));
481
- testFormData.set('testFileOverrides', JSON.stringify(Object.fromEntries(Object.entries(flowOverrides).map(([key, value]) => [
482
- key.replaceAll(commonRoot, '.').split(path.sep).join('/'),
483
- value,
484
- ]))));
485
- testFormData.set('sequentialFlows', JSON.stringify(sequentialFlows.map((t) => t.replaceAll(commonRoot, '.').split(path.sep).join('/'))));
486
- testFormData.set('env', JSON.stringify(envObject));
487
- testFormData.set('googlePlay', googlePlay ? 'true' : 'false');
488
- const config = {
489
- allExcludeTags,
490
- allIncludeTags,
491
- autoRetriesRemaining: retry,
319
+ const testFormData = await this.testSubmissionService.buildTestFormData({
320
+ androidApiLevel,
321
+ androidDevice,
322
+ appBinaryId: finalBinaryId,
323
+ cliVersion: this.config.version,
324
+ commonRoot,
492
325
  continueOnFailure,
326
+ debug,
493
327
  deviceLocale,
328
+ env,
329
+ executionPlan,
330
+ flowFile,
331
+ googlePlay,
332
+ iOSDevice,
333
+ iOSVersion,
334
+ logger: this.log.bind(this),
494
335
  maestroVersion: resolvedMaestroVersion,
336
+ metadata,
495
337
  mitmHost,
496
338
  mitmPath,
339
+ name,
497
340
  orientation,
498
- raw: JSON.stringify(raw),
341
+ raw,
499
342
  report,
343
+ retry,
344
+ runnerType,
500
345
  showCrosshairs: flags['show-crosshairs'],
501
346
  skipChromeOnboarding: flags['skip-chrome-onboarding'],
502
- version: this.config.version,
503
- };
504
- testFormData.set('config', JSON.stringify(config));
505
- if (Object.keys(metadataObject).length > 0) {
506
- const metadataPayload = { userMetadata: metadataObject };
507
- testFormData.set('metadata', JSON.stringify(metadataPayload));
508
- if (debug) {
509
- this.log(`DEBUG: Sending metadata to API: ${JSON.stringify(metadataPayload)}`);
510
- }
511
- }
512
- if (androidApiLevel)
513
- testFormData.set('androidApiLevel', androidApiLevel.toString());
514
- if (androidDevice)
515
- testFormData.set('androidDevice', androidDevice.toString());
516
- if (iOSVersion)
517
- testFormData.set('iOSVersion', iOSVersion.toString());
518
- if (iOSDevice)
519
- testFormData.set('iOSDevice', iOSDevice.toString());
520
- if (name)
521
- testFormData.set('name', name.toString());
522
- if (runnerType)
523
- testFormData.set('runnerType', runnerType.toString());
524
- if (workspaceConfig)
525
- testFormData.set('workspaceConfig', JSON.stringify(workspaceConfig));
526
- for (const [key, value] of Object.entries(rest)) {
527
- if (value) {
528
- testFormData.set(key, value);
529
- }
530
- }
347
+ });
531
348
  if (debug) {
532
349
  this.log(`DEBUG: Submitting flow upload request to ${apiUrl}/uploads/flow`);
533
350
  }
@@ -573,215 +390,96 @@ class Cloud extends core_1.Command {
573
390
  this.log('Not waiting for results as async flag is set to true');
574
391
  return;
575
392
  }
576
- // poll for the run status every 5 seconds
577
- if (!json) {
578
- core_1.ux.action.start('Waiting for results', 'Initializing', {
579
- stdout: true,
580
- });
581
- this.log('\nYou can safely close this terminal and the tests will continue\n');
582
- }
583
- let sequentialPollFaillures = 0;
584
- let previousSummary = '';
585
- if (debug) {
586
- this.log(`DEBUG: Starting polling loop for results`);
587
- }
588
- await new Promise((resolve, reject) => {
589
- const intervalId = setInterval(async () => {
590
- try {
591
- if (debug) {
592
- this.log(`DEBUG: Polling for results: ${results[0].test_upload_id}`);
593
- }
594
- const { results: updatedResults } = await api_gateway_1.ApiGateway.getResultsForUpload(apiUrl, apiKey, results[0].test_upload_id);
595
- if (!updatedResults) {
596
- throw new Error('no results');
597
- }
598
- if (debug) {
599
- this.log(`DEBUG: Poll received ${updatedResults.length} results`);
600
- for (const result of updatedResults) {
601
- this.log(`DEBUG: Result status: ${result.test_file_name} - ${result.status}`);
602
- }
603
- }
604
- // Calculate summary statistics
605
- const statusCounts = {};
606
- for (const result of updatedResults) {
607
- statusCounts[result.status] = (statusCounts[result.status] || 0) + 1;
608
- }
609
- const passed = statusCounts.PASSED || 0;
610
- const failed = statusCounts.FAILED || 0;
611
- const pending = statusCounts.PENDING || 0;
612
- const running = statusCounts.RUNNING || 0;
613
- const total = updatedResults.length;
614
- const completed = passed + failed;
615
- // Display quantitative summary in quiet mode only
616
- const summary = `${completed}/${total} | ✓ ${passed} | ✗ ${failed} | ▶ ${running} | ⏸ ${pending}`;
617
- if (!json) {
618
- if (quiet) {
619
- // Only update status when the summary changes in quiet mode
620
- if (summary !== previousSummary) {
621
- core_1.ux.action.status = summary;
622
- previousSummary = summary;
623
- }
624
- }
625
- else {
626
- // Show detailed table in non-quiet mode (no summary)
627
- core_1.ux.action.status =
628
- '\nStatus Test\n─────────── ───────────────';
629
- for (const { retry_of: isRetry, status, test_file_name: test, } of updatedResults) {
630
- core_1.ux.action.status += `\n${status.padEnd(10, ' ')} ${test} ${isRetry ? '(retry)' : ''}`;
631
- }
632
- }
633
- }
634
- if (updatedResults.every((result) => !['PENDING', 'RUNNING'].includes(result.status))) {
635
- if (debug) {
636
- this.log(`DEBUG: All tests completed, stopping poll`);
637
- }
638
- if (!json) {
639
- core_1.ux.action.stop('completed');
640
- this.log('\n');
641
- const hasFailedTests = updatedResults.some((result) => result.status === 'FAILED');
642
- (0, cli_ux_1.table)(updatedResults, {
643
- status: { get: (row) => row.status },
644
- test: {
645
- get: (row) => `${row.test_file_name} ${row.retry_of ? '(retry)' : ''}`,
646
- },
647
- duration: {
648
- get: (row) => row.duration_seconds
649
- ? (0, methods_1.formatDurationSeconds)(Number(row.duration_seconds))
650
- : '-',
651
- },
652
- ...(hasFailedTests && {
653
- // eslint-disable-next-line camelcase
654
- fail_reason: {
655
- get: (row) => row.status === 'FAILED' && row.fail_reason
656
- ? row.fail_reason
657
- : '',
658
- },
659
- }),
660
- }, { printLine: this.log.bind(this) });
661
- this.log('\n');
662
- this.log('Run completed, you can access the results at:');
663
- this.log(url);
664
- this.log('\n');
665
- }
666
- clearInterval(intervalId);
667
- if (downloadArtifacts) {
668
- try {
669
- if (debug) {
670
- this.log(`DEBUG: Downloading artifacts: ${downloadArtifacts}`);
671
- }
672
- await api_gateway_1.ApiGateway.downloadArtifactsZip(apiUrl, apiKey, results[0].test_upload_id, downloadArtifacts, artifactsPath);
673
- this.log('\n');
674
- this.log(`Test artifacts have been downloaded to ${artifactsPath || './artifacts.zip'}`);
675
- }
676
- catch (error) {
677
- if (debug) {
678
- this.log(`DEBUG: Error downloading artifacts: ${error}`);
679
- }
680
- this.warn('Failed to download artifacts');
681
- }
682
- }
683
- // Handle report downloads based on --report flag
684
- if (report && ['allure', 'html', 'junit'].includes(report)) {
685
- await this.handleReportDownloads(report, apiUrl, apiKey, results[0].test_upload_id, junitPath, allurePath, htmlPath, debug);
686
- }
687
- const resultsWithoutEarlierTries = updatedResults.filter((result) => {
688
- const originalTryId = result.retry_of || result.id;
689
- const tries = updatedResults.filter((r) => r.retry_of === originalTryId || r.id === originalTryId);
690
- return result.id === Math.max(...tries.map((t) => t.id));
691
- });
692
- if (resultsWithoutEarlierTries.some((result) => result.status === 'FAILED')) {
693
- if (debug) {
694
- this.log(`DEBUG: Some tests failed, returning failed status`);
695
- }
696
- const jsonOutput = {
697
- consoleUrl: url,
698
- status: 'FAILED',
699
- tests: resultsWithoutEarlierTries.map((r) => ({
700
- name: r.test_file_name,
701
- status: r.status,
702
- durationSeconds: r.duration_seconds,
703
- failReason: r.status === 'FAILED'
704
- ? r.fail_reason || 'No reason provided'
705
- : undefined,
706
- })),
707
- uploadId: results[0].test_upload_id,
708
- };
709
- if (flags['json-file']) {
710
- const jsonFilePath = jsonFileName || `${results[0].test_upload_id}_dcd.json`;
711
- (0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput, this);
712
- }
713
- if (json) {
714
- output = jsonOutput;
715
- }
716
- reject(new Error('RUN_FAILED'));
717
- }
718
- else {
719
- if (debug) {
720
- this.log(`DEBUG: All tests passed, returning success status`);
721
- }
722
- const jsonOutput = {
723
- consoleUrl: url,
724
- status: 'PASSED',
725
- tests: resultsWithoutEarlierTries.map((r) => ({
726
- name: r.test_file_name,
727
- status: r.status,
728
- durationSeconds: r.duration_seconds,
729
- failReason: r.status === 'FAILED'
730
- ? r.fail_reason || 'No reason provided'
731
- : undefined,
732
- })),
733
- uploadId: results[0].test_upload_id,
734
- };
735
- if (flags['json-file']) {
736
- const jsonFilePath = jsonFileName || `${results[0].test_upload_id}_dcd.json`;
737
- (0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput, this);
738
- }
739
- if (json) {
740
- output = jsonOutput;
741
- }
742
- }
743
- sequentialPollFaillures = 0;
744
- resolve();
745
- }
393
+ // Poll for results until completion
394
+ const pollingResult = await this.resultsPollingService
395
+ .pollUntilComplete(results, {
396
+ apiKey,
397
+ apiUrl,
398
+ consoleUrl: url,
399
+ debug,
400
+ json,
401
+ logger: this.log.bind(this),
402
+ quiet,
403
+ uploadId: results[0].test_upload_id,
404
+ })
405
+ .catch(async (error) => {
406
+ if (error instanceof results_polling_service_1.RunFailedError) {
407
+ // Handle failed test run
408
+ const jsonOutput = error.result;
409
+ if (flags['json-file']) {
410
+ const jsonFilePath = jsonFileName || `${results[0].test_upload_id}_dcd.json`;
411
+ (0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput, this);
746
412
  }
747
- catch (error) {
748
- sequentialPollFaillures++;
749
- if (debug) {
750
- this.log(`DEBUG: Error polling for results: ${error}`);
751
- this.log(`DEBUG: Sequential poll failures: ${sequentialPollFaillures}`);
752
- }
753
- if (sequentialPollFaillures > 10) {
754
- // dropped poll requests shouldn't err user CI
755
- clearInterval(intervalId);
756
- // Check if the failure is due to internet connectivity issues
757
- if (debug) {
758
- this.log('DEBUG: Checking internet connectivity...');
759
- }
760
- const connectivityCheck = await (0, connectivity_1.checkInternetConnectivity)();
761
- if (debug) {
762
- this.log(`DEBUG: ${connectivityCheck.message}`);
763
- for (const result of connectivityCheck.endpointResults) {
764
- if (result.success) {
765
- this.log(`DEBUG: ✓ ${result.endpoint} - ${result.statusCode} (${result.latencyMs}ms)`);
766
- }
767
- else {
768
- this.log(`DEBUG: ✗ ${result.endpoint} - ${result.error} (${result.latencyMs}ms)`);
769
- }
770
- }
771
- }
772
- if (!connectivityCheck.connected) {
773
- // Build detailed error message with endpoint diagnostics
774
- const endpointDetails = connectivityCheck.endpointResults
775
- .map((r) => ` - ${r.endpoint}: ${r.error} (${r.latencyMs}ms)`)
776
- .join('\n');
777
- throw new Error(`Unable to fetch results after 10 attempts.\n\nInternet connectivity check failed - all test endpoints unreachable:\n${endpointDetails}\n\nPlease verify your network connection and DNS resolution.`);
778
- }
779
- throw new Error('unable to fetch results after 10 attempts');
780
- }
781
- this.log('unable to fetch results, trying again...');
413
+ if (json) {
414
+ output = jsonOutput;
415
+ }
416
+ // Download artifacts and reports even when tests fail
417
+ if (downloadArtifacts) {
418
+ await this.reportDownloadService.downloadArtifacts({
419
+ apiKey,
420
+ apiUrl,
421
+ artifactsPath,
422
+ debug,
423
+ downloadType: downloadArtifacts,
424
+ logger: this.log.bind(this),
425
+ uploadId: results[0].test_upload_id,
426
+ warnLogger: this.warn.bind(this),
427
+ });
428
+ }
429
+ if (report && ['allure', 'html', 'junit'].includes(report)) {
430
+ await this.reportDownloadService.downloadReports({
431
+ allurePath,
432
+ apiKey,
433
+ apiUrl,
434
+ debug,
435
+ htmlPath,
436
+ junitPath,
437
+ logger: this.log.bind(this),
438
+ reportType: report,
439
+ uploadId: results[0].test_upload_id,
440
+ warnLogger: this.warn.bind(this),
441
+ });
782
442
  }
783
- }, 5000);
443
+ throw new Error('RUN_FAILED');
444
+ }
445
+ throw error;
784
446
  });
447
+ // Handle successful completion - download artifacts and reports
448
+ if (downloadArtifacts) {
449
+ await this.reportDownloadService.downloadArtifacts({
450
+ apiKey,
451
+ apiUrl,
452
+ artifactsPath,
453
+ debug,
454
+ downloadType: downloadArtifacts,
455
+ logger: this.log.bind(this),
456
+ uploadId: results[0].test_upload_id,
457
+ warnLogger: this.warn.bind(this),
458
+ });
459
+ }
460
+ // Handle report downloads based on --report flag
461
+ if (report && ['allure', 'html', 'junit'].includes(report)) {
462
+ await this.reportDownloadService.downloadReports({
463
+ allurePath,
464
+ apiKey,
465
+ apiUrl,
466
+ debug,
467
+ htmlPath,
468
+ junitPath,
469
+ logger: this.log.bind(this),
470
+ reportType: report,
471
+ uploadId: results[0].test_upload_id,
472
+ warnLogger: this.warn.bind(this),
473
+ });
474
+ }
475
+ const jsonOutput = pollingResult;
476
+ if (flags['json-file']) {
477
+ const jsonFilePath = jsonFileName || `${results[0].test_upload_id}_dcd.json`;
478
+ (0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput, this);
479
+ }
480
+ if (json) {
481
+ output = jsonOutput;
482
+ }
785
483
  }
786
484
  catch (error) {
787
485
  if (debugFlag && error instanceof Error) {
@@ -806,65 +504,5 @@ class Cloud extends core_1.Command {
806
504
  }
807
505
  }
808
506
  }
809
- /**
810
- * Handle downloading reports based on the report type specified
811
- * @param reportType The type of report to download ('junit', 'allure', or 'html')
812
- * @param apiUrl The base URL for the API server
813
- * @param apiKey The API key for authentication
814
- * @param uploadId The unique identifier for the test upload
815
- * @param junitPath Optional file path where the JUnit report should be saved
816
- * @param allurePath Optional file path where the Allure report should be saved
817
- * @param htmlPath Optional file path where the HTML report should be saved
818
- * @param debug Whether debug logging is enabled
819
- * @returns Promise that resolves when all reports have been downloaded
820
- */
821
- // eslint-disable-next-line max-params
822
- async handleReportDownloads(reportType, apiUrl, apiKey, uploadId, junitPath, allurePath, htmlPath, debug) {
823
- const downloadReport = async (type, filePath) => {
824
- try {
825
- if (debug) {
826
- this.log(`DEBUG: Downloading ${type.toUpperCase()} report`);
827
- }
828
- await api_gateway_1.ApiGateway.downloadReportGeneric(apiUrl, apiKey, uploadId, type, filePath);
829
- this.log(`${type.toUpperCase()} test report has been downloaded to ${filePath}`);
830
- }
831
- catch (error) {
832
- if (debug) {
833
- this.log(`DEBUG: Error downloading ${type.toUpperCase()} report: ${error}`);
834
- }
835
- const errorMessage = error instanceof Error ? error.message : String(error);
836
- this.warn(`Failed to download ${type.toUpperCase()} report: ${errorMessage}`);
837
- if (errorMessage.includes('404')) {
838
- this.warn(`No ${type.toUpperCase()} reports found for this upload. Make sure your tests generated results.`);
839
- }
840
- else if (errorMessage.includes('EACCES') || errorMessage.includes('EPERM')) {
841
- this.warn('Permission denied. Check write permissions for the current directory.');
842
- }
843
- else if (errorMessage.includes('ENOENT')) {
844
- this.warn('Directory does not exist. Make sure you have write access to the current directory.');
845
- }
846
- }
847
- };
848
- switch (reportType) {
849
- case 'junit': {
850
- const reportPath = path.resolve(process.cwd(), junitPath || 'report.xml');
851
- await downloadReport('junit', reportPath);
852
- break;
853
- }
854
- case 'allure': {
855
- const reportPath = path.resolve(process.cwd(), allurePath || 'report.html');
856
- await downloadReport('allure', reportPath);
857
- break;
858
- }
859
- case 'html': {
860
- const htmlReportPath = path.resolve(process.cwd(), htmlPath || 'report.html');
861
- await downloadReport('html', htmlReportPath);
862
- break;
863
- }
864
- default: {
865
- this.warn(`Unknown report type: ${reportType}`);
866
- }
867
- }
868
- }
869
507
  }
870
508
  exports.default = Cloud;