@devicecloud.dev/dcd 4.1.2-beta.1 → 4.1.3

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