@devicecloud.dev/dcd 4.0.2 → 4.0.3-beta.1

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.
@@ -54,4 +54,15 @@ export default class Cloud extends Command {
54
54
  };
55
55
  private versionCheck;
56
56
  run(): Promise<any>;
57
+ /**
58
+ * Handle downloading reports based on the report type specified
59
+ * @param reportType The type of report to download ('junit', 'allure', or 'all')
60
+ * @param apiUrl The base URL for the API server
61
+ * @param apiKey The API key for authentication
62
+ * @param uploadId The unique identifier for the test upload
63
+ * @param junitPath Optional file path where the JUnit report should be saved
64
+ * @param debug Whether debug logging is enabled
65
+ * @returns Promise that resolves when all reports have been downloaded
66
+ */
67
+ private handleReportDownloads;
57
68
  }
@@ -684,33 +684,9 @@ class Cloud extends core_1.Command {
684
684
  this.warn('Failed to download artifacts');
685
685
  }
686
686
  }
687
- // Handle report download separately if --report junit is specified
688
- if (report === 'junit') {
689
- try {
690
- if (debug) {
691
- this.log('DEBUG: Downloading JUNIT report');
692
- }
693
- const reportPath = path.resolve(process.cwd(), junitPath || 'report.xml');
694
- await api_gateway_1.ApiGateway.downloadReport(apiUrl, apiKey, results[0].test_upload_id, reportPath);
695
- this.log('\n');
696
- this.log(`JUNIT test report has been downloaded to ${reportPath}`);
697
- }
698
- catch (error) {
699
- if (debug) {
700
- this.log(`DEBUG: Error downloading JUNIT report: ${error}`);
701
- }
702
- const errorMessage = error instanceof Error ? error.message : String(error);
703
- this.warn(`Failed to download JUNIT report: ${errorMessage}`);
704
- if (errorMessage.includes('404')) {
705
- this.warn('No JUNIT reports found for this upload. Make sure your tests were run with --report junit flag.');
706
- }
707
- else if (errorMessage.includes('EACCES') || errorMessage.includes('EPERM')) {
708
- this.warn('Permission denied. Check write permissions for the current directory.');
709
- }
710
- else if (errorMessage.includes('ENOENT')) {
711
- this.warn('Directory does not exist. Make sure you have write access to the current directory.');
712
- }
713
- }
687
+ // Handle report downloads based on --report flag
688
+ if (report && ['allure', 'html', 'junit'].includes(report)) {
689
+ await this.handleReportDownloads(report, apiUrl, apiKey, results[0].test_upload_id, junitPath, debug);
714
690
  }
715
691
  const resultsWithoutEarlierTries = updatedResults.filter((result) => {
716
692
  const originalTryId = result.retry_of || result.id;
@@ -811,5 +787,63 @@ class Cloud extends core_1.Command {
811
787
  }
812
788
  }
813
789
  }
790
+ /**
791
+ * Handle downloading reports based on the report type specified
792
+ * @param reportType The type of report to download ('junit', 'allure', or 'all')
793
+ * @param apiUrl The base URL for the API server
794
+ * @param apiKey The API key for authentication
795
+ * @param uploadId The unique identifier for the test upload
796
+ * @param junitPath Optional file path where the JUnit report should be saved
797
+ * @param debug Whether debug logging is enabled
798
+ * @returns Promise that resolves when all reports have been downloaded
799
+ */
800
+ // eslint-disable-next-line max-params
801
+ async handleReportDownloads(reportType, apiUrl, apiKey, uploadId, junitPath, debug) {
802
+ const downloadReport = async (type, filePath) => {
803
+ try {
804
+ if (debug) {
805
+ this.log(`DEBUG: Downloading ${type.toUpperCase()} report`);
806
+ }
807
+ await api_gateway_1.ApiGateway.downloadReportGeneric(apiUrl, apiKey, uploadId, type, filePath);
808
+ this.log(`${type.toUpperCase()} test report has been downloaded to ${filePath}`);
809
+ }
810
+ catch (error) {
811
+ if (debug) {
812
+ this.log(`DEBUG: Error downloading ${type.toUpperCase()} report: ${error}`);
813
+ }
814
+ const errorMessage = error instanceof Error ? error.message : String(error);
815
+ this.warn(`Failed to download ${type.toUpperCase()} report: ${errorMessage}`);
816
+ if (errorMessage.includes('404')) {
817
+ this.warn(`No ${type.toUpperCase()} reports found for this upload. Make sure your tests generated results.`);
818
+ }
819
+ else if (errorMessage.includes('EACCES') || errorMessage.includes('EPERM')) {
820
+ this.warn('Permission denied. Check write permissions for the current directory.');
821
+ }
822
+ else if (errorMessage.includes('ENOENT')) {
823
+ this.warn('Directory does not exist. Make sure you have write access to the current directory.');
824
+ }
825
+ }
826
+ };
827
+ switch (reportType) {
828
+ case 'junit': {
829
+ const reportPath = path.resolve(process.cwd(), junitPath || 'report.xml');
830
+ await downloadReport('junit', reportPath);
831
+ break;
832
+ }
833
+ case 'allure': {
834
+ const allureReportPath = path.resolve(process.cwd(), `allure-report-${uploadId}.zip`);
835
+ await downloadReport('allure', allureReportPath);
836
+ break;
837
+ }
838
+ case 'html': {
839
+ const htmlReportPath = path.resolve(process.cwd(), `html-report-${uploadId}.zip`);
840
+ await downloadReport('html', htmlReportPath);
841
+ break;
842
+ }
843
+ default: {
844
+ this.warn(`Unknown report type: ${reportType}`);
845
+ }
846
+ }
847
+ }
814
848
  }
815
849
  exports.default = Cloud;
package/dist/constants.js CHANGED
@@ -194,8 +194,8 @@ exports.flags = {
194
194
  }),
195
195
  report: core_1.Flags.string({
196
196
  aliases: ['format'],
197
- description: 'Runs Maestro with the --format flag, this will generate a report in the specified format',
198
- options: ['junit', 'html'],
197
+ description: 'Generate and download test reports in the specified format. HTML reports will be returned in the Allure format, please see the documentation for more details.',
198
+ options: ['junit', 'allure', 'html'],
199
199
  }),
200
200
  retry: core_1.Flags.integer({
201
201
  description: 'Automatically retry the run up to the number of times specified (same as pressing retry in the UI) - this is free of charge',
@@ -1,10 +1,16 @@
1
1
  import { TAppMetadata } from '../types';
2
2
  export declare const ApiGateway: {
3
+ /**
4
+ * Standardized error handling for API responses
5
+ * @param res - The fetch response object
6
+ * @param operation - Description of the operation that failed
7
+ * @returns Never returns, always throws
8
+ */
9
+ handleApiError(res: Response, operation: string): Promise<never>;
3
10
  checkForExistingUpload(baseUrl: string, apiKey: string, sha: string): Promise<{
4
11
  appBinaryId: string;
5
12
  exists: boolean;
6
13
  }>;
7
- downloadReport(baseUrl: string, apiKey: string, uploadId: string, reportPath?: string): Promise<void>;
8
14
  downloadArtifactsZip(baseUrl: string, apiKey: string, uploadId: string, results: "ALL" | "FAILED", artifactsPath?: string): Promise<void>;
9
15
  finaliseUpload(baseUrl: string, apiKey: string, id: string, metadata: TAppMetadata, path: string, sha: string): Promise<Record<string, never>>;
10
16
  getBinaryUploadUrl(baseUrl: string, apiKey: string, platform: "android" | "ios"): Promise<{
@@ -33,4 +39,14 @@ export declare const ApiGateway: {
33
39
  message?: string;
34
40
  results?: import("../types/schema.types").components["schemas"]["IDBResult"][];
35
41
  }>;
42
+ /**
43
+ * Generic report download method that handles both junit and allure reports
44
+ * @param baseUrl - API base URL
45
+ * @param apiKey - API key for authentication
46
+ * @param uploadId - Upload ID to download report for
47
+ * @param reportType - Type of report to download ('junit' or 'allure')
48
+ * @param reportPath - Optional custom path for the downloaded report
49
+ * @returns Promise that resolves when download is complete
50
+ */
51
+ downloadReportGeneric(baseUrl: string, apiKey: string, uploadId: string, reportType: "allure" | "junit" | "html", reportPath?: string): Promise<void>;
36
52
  };
@@ -1,9 +1,50 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ApiGateway = void 0;
4
- const fs = require("node:fs/promises");
5
4
  const path = require("node:path");
6
5
  exports.ApiGateway = {
6
+ /**
7
+ * Standardized error handling for API responses
8
+ * @param res - The fetch response object
9
+ * @param operation - Description of the operation that failed
10
+ * @returns Never returns, always throws
11
+ */
12
+ async handleApiError(res, operation) {
13
+ const errorText = await res.text();
14
+ let userMessage;
15
+ // Parse common API error formats
16
+ try {
17
+ const errorData = JSON.parse(errorText);
18
+ userMessage = errorData.message || errorData.error || errorText;
19
+ }
20
+ catch {
21
+ userMessage = errorText;
22
+ }
23
+ // Add context and improve readability
24
+ switch (res.status) {
25
+ case 400: {
26
+ throw new Error(`Invalid request: ${userMessage}`);
27
+ }
28
+ case 401: {
29
+ throw new Error(`Authentication failed. Please check your API key.`);
30
+ }
31
+ case 403: {
32
+ throw new Error(`Access denied. ${userMessage}`);
33
+ }
34
+ case 404: {
35
+ throw new Error(`Resource not found. ${userMessage}`);
36
+ }
37
+ case 429: {
38
+ throw new Error(`Rate limit exceeded. Please try again later.`);
39
+ }
40
+ case 500: {
41
+ throw new Error(`Server error occurred. Please try again or contact support.`);
42
+ }
43
+ default: {
44
+ throw new Error(`${operation} failed: ${userMessage} (HTTP ${res.status})`);
45
+ }
46
+ }
47
+ },
7
48
  async checkForExistingUpload(baseUrl, apiKey, sha) {
8
49
  const res = await fetch(`${baseUrl}/uploads/checkForExistingUpload`, {
9
50
  body: JSON.stringify({ sha }),
@@ -15,25 +56,6 @@ exports.ApiGateway = {
15
56
  });
16
57
  return res.json();
17
58
  },
18
- async downloadReport(baseUrl, apiKey, uploadId, reportPath) {
19
- const finalReportPath = reportPath || path.resolve(process.cwd(), `report-${uploadId}.xml`);
20
- const url = `${baseUrl}/results/${uploadId}/report`;
21
- const res = await fetch(url, {
22
- headers: {
23
- 'x-app-api-key': apiKey,
24
- },
25
- method: 'GET',
26
- });
27
- if (!res.ok) {
28
- const errorText = await res.text();
29
- if (res.status === 404) {
30
- throw new Error(`Upload ID '${uploadId}' not found or no results available for this upload`);
31
- }
32
- throw new Error(`Failed to download report: ${res.status} ${errorText}`);
33
- }
34
- const buffer = await res.arrayBuffer();
35
- await fs.writeFile(finalReportPath, Buffer.from(buffer));
36
- },
37
59
  async downloadArtifactsZip(baseUrl, apiKey, uploadId, results, artifactsPath = './artifacts.zip') {
38
60
  const res = await fetch(`${baseUrl}/results/${uploadId}/download`, {
39
61
  body: JSON.stringify({ results }),
@@ -44,7 +66,7 @@ exports.ApiGateway = {
44
66
  method: 'POST',
45
67
  });
46
68
  if (!res.ok) {
47
- throw new Error(await res.text());
69
+ await this.handleApiError(res, 'Failed to download artifacts');
48
70
  }
49
71
  // Handle tilde expansion for home directory
50
72
  if (artifactsPath.startsWith('~/') || artifactsPath === '~') {
@@ -87,7 +109,7 @@ exports.ApiGateway = {
87
109
  method: 'POST',
88
110
  });
89
111
  if (!res.ok) {
90
- throw new Error(await res.text());
112
+ await this.handleApiError(res, 'Failed to finalize upload');
91
113
  }
92
114
  return res.json();
93
115
  },
@@ -101,7 +123,7 @@ exports.ApiGateway = {
101
123
  method: 'POST',
102
124
  });
103
125
  if (!res.ok) {
104
- throw new Error(await res.text());
126
+ await this.handleApiError(res, 'Failed to get upload URL');
105
127
  }
106
128
  return res.json();
107
129
  },
@@ -111,7 +133,7 @@ exports.ApiGateway = {
111
133
  headers: { 'x-app-api-key': apiKey },
112
134
  });
113
135
  if (!res.ok) {
114
- throw new Error(await res.text());
136
+ await this.handleApiError(res, 'Failed to get results');
115
137
  }
116
138
  return res.json();
117
139
  },
@@ -129,7 +151,7 @@ exports.ApiGateway = {
129
151
  },
130
152
  });
131
153
  if (!response.ok) {
132
- throw new Error(`Failed to fetch status: ${response.statusText}`);
154
+ await this.handleApiError(response, 'Failed to get upload status');
133
155
  }
134
156
  return response.json();
135
157
  },
@@ -142,8 +164,88 @@ exports.ApiGateway = {
142
164
  method: 'POST',
143
165
  });
144
166
  if (!res.ok) {
145
- throw new Error(await res.text());
167
+ await this.handleApiError(res, 'Failed to upload test flows');
146
168
  }
147
169
  return res.json();
148
170
  },
171
+ /**
172
+ * Generic report download method that handles both junit and allure reports
173
+ * @param baseUrl - API base URL
174
+ * @param apiKey - API key for authentication
175
+ * @param uploadId - Upload ID to download report for
176
+ * @param reportType - Type of report to download ('junit' or 'allure')
177
+ * @param reportPath - Optional custom path for the downloaded report
178
+ * @returns Promise that resolves when download is complete
179
+ */
180
+ async downloadReportGeneric(baseUrl, apiKey, uploadId, reportType, reportPath) {
181
+ // Define endpoint and default filename mappings
182
+ const config = {
183
+ junit: {
184
+ endpoint: `/results/${uploadId}/report`,
185
+ defaultFilename: `report-${uploadId}.xml`,
186
+ notFoundMessage: `Upload ID '${uploadId}' not found or no results available for this upload`,
187
+ errorPrefix: 'Failed to download report',
188
+ },
189
+ allure: {
190
+ endpoint: `/allure/${uploadId}/download`,
191
+ defaultFilename: `allure-report-${uploadId}.zip`,
192
+ notFoundMessage: `Upload ID '${uploadId}' not found or no Allure report available for this upload`,
193
+ errorPrefix: 'Failed to download Allure report',
194
+ },
195
+ html: {
196
+ endpoint: `/results/${uploadId}/html-report`,
197
+ defaultFilename: `html-report-${uploadId}.zip`,
198
+ notFoundMessage: `Upload ID '${uploadId}' not found or no HTML report available for this upload`,
199
+ errorPrefix: 'Failed to download HTML report',
200
+ },
201
+ };
202
+ const { endpoint, defaultFilename, notFoundMessage, errorPrefix } = config[reportType];
203
+ const finalReportPath = reportPath || path.resolve(process.cwd(), defaultFilename);
204
+ const url = `${baseUrl}${endpoint}`;
205
+ // Make the download request
206
+ const res = await fetch(url, {
207
+ headers: {
208
+ 'x-app-api-key': apiKey,
209
+ },
210
+ method: 'GET',
211
+ });
212
+ if (!res.ok) {
213
+ const errorText = await res.text();
214
+ if (res.status === 404) {
215
+ throw new Error(notFoundMessage);
216
+ }
217
+ throw new Error(`${errorPrefix}: ${res.status} ${errorText}`);
218
+ }
219
+ // Handle tilde expansion for home directory (applies to all report types)
220
+ let expandedPath = finalReportPath;
221
+ if (finalReportPath.startsWith('~/') || finalReportPath === '~') {
222
+ expandedPath = finalReportPath.replace(/^~/,
223
+ // eslint-disable-next-line unicorn/prefer-module
224
+ require('node:os').homedir());
225
+ }
226
+ // Create directory structure if it doesn't exist
227
+ // eslint-disable-next-line unicorn/prefer-module
228
+ const { dirname } = require('node:path');
229
+ // eslint-disable-next-line unicorn/prefer-module
230
+ const { createWriteStream, mkdirSync } = require('node:fs');
231
+ // eslint-disable-next-line unicorn/prefer-module
232
+ const { finished } = require('node:stream/promises');
233
+ // eslint-disable-next-line unicorn/prefer-module
234
+ const { Readable } = require('node:stream');
235
+ const directory = dirname(expandedPath);
236
+ if (directory !== '.') {
237
+ try {
238
+ mkdirSync(directory, { recursive: true });
239
+ }
240
+ catch (error) {
241
+ // Ignore EEXIST errors (directory already exists)
242
+ if (error.code !== 'EEXIST') {
243
+ throw error;
244
+ }
245
+ }
246
+ }
247
+ // Write the file using streaming for better memory efficiency
248
+ const fileStream = createWriteStream(expandedPath, { flags: 'wx' });
249
+ await finished(Readable.fromWeb(res.body).pipe(fileStream));
250
+ },
149
251
  };
@@ -374,12 +374,13 @@
374
374
  "aliases": [
375
375
  "format"
376
376
  ],
377
- "description": "Runs Maestro with the --format flag, this will generate a report in the specified format",
377
+ "description": "Generate and download test reports in the specified format. HTML reports will be returned in the Allure format, please see the documentation for more details.",
378
378
  "name": "report",
379
379
  "hasDynamicHelp": false,
380
380
  "multiple": false,
381
381
  "options": [
382
382
  "junit",
383
+ "allure",
383
384
  "html"
384
385
  ],
385
386
  "type": "option"
@@ -581,5 +582,5 @@
581
582
  ]
582
583
  }
583
584
  },
584
- "version": "4.0.2"
585
+ "version": "4.0.3-beta.1"
585
586
  }
package/package.json CHANGED
@@ -80,7 +80,7 @@
80
80
  "version": "oclif readme && git add README.md",
81
81
  "test": "node scripts/test-runner.mjs"
82
82
  },
83
- "version": "4.0.2",
83
+ "version": "4.0.3-beta.1",
84
84
  "bugs": {
85
85
  "url": "https://discord.gg/gm3mJwcNw8"
86
86
  },
@@ -94,4 +94,4 @@
94
94
  ],
95
95
  "types": "dist/index.d.ts",
96
96
  "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
97
- }
97
+ }