@devicecloud.dev/dcd 4.0.1 → 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.
- package/dist/commands/cloud.d.ts +10 -5
- package/dist/commands/cloud.js +65 -12
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +9 -5
- package/dist/gateways/api-gateway.d.ts +19 -2
- package/dist/gateways/api-gateway.js +129 -6
- package/dist/types/schema.types.d.ts +310 -313
- package/dist/types/schema.types.js +1 -4
- package/oclif.manifest.json +17 -5
- package/package.json +4 -4
package/dist/commands/cloud.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export default class Cloud extends Command {
|
|
|
18
18
|
'app-binary-id': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
19
19
|
'app-file': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
20
20
|
'artifacts-path': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
21
|
+
'junit-path': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
21
22
|
async: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
22
23
|
config: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
23
24
|
debug: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
@@ -54,10 +55,14 @@ export default class Cloud extends Command {
|
|
|
54
55
|
private versionCheck;
|
|
55
56
|
run(): Promise<any>;
|
|
56
57
|
/**
|
|
57
|
-
*
|
|
58
|
-
* @param
|
|
59
|
-
* @param
|
|
60
|
-
* @
|
|
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
|
|
61
66
|
*/
|
|
62
|
-
private
|
|
67
|
+
private handleReportDownloads;
|
|
63
68
|
}
|
package/dist/commands/cloud.js
CHANGED
|
@@ -64,7 +64,7 @@ class Cloud extends core_1.Command {
|
|
|
64
64
|
let jsonFile = false;
|
|
65
65
|
try {
|
|
66
66
|
const { args, flags, raw } = await this.parse(Cloud);
|
|
67
|
-
let { 'additional-app-binary-ids': nonFlatAdditionalAppBinaryIds, 'additional-app-files': nonFlatAdditionalAppFiles, 'android-api-level': androidApiLevel, 'android-device': androidDevice, apiKey: apiKeyFlag, apiUrl, 'app-binary-id': appBinaryId, 'app-file': appFile, 'artifacts-path': artifactsPath, 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, 'x86-arch': x86Arch, ...rest } = flags;
|
|
67
|
+
let { 'additional-app-binary-ids': nonFlatAdditionalAppBinaryIds, 'additional-app-files': nonFlatAdditionalAppFiles, 'android-api-level': androidApiLevel, 'android-device': androidDevice, apiKey: apiKeyFlag, apiUrl, 'app-binary-id': appBinaryId, 'app-file': appFile, 'artifacts-path': artifactsPath, 'junit-path': junitPath, 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, 'x86-arch': x86Arch, ...rest } = flags;
|
|
68
68
|
// Resolve "latest" maestro version to actual version
|
|
69
69
|
const resolvedMaestroVersion = (0, constants_1.resolveMaestroVersion)(maestroVersion);
|
|
70
70
|
// Store debug flag for use in catch block
|
|
@@ -228,6 +228,9 @@ class Cloud extends core_1.Command {
|
|
|
228
228
|
if (runnerType === 'm1') {
|
|
229
229
|
this.log('Note: runnerType m1 is experimental and currently supports Android (Pixel 7, API Level 34) only.');
|
|
230
230
|
}
|
|
231
|
+
if (runnerType === 'gpu1') {
|
|
232
|
+
this.log('Note: runnerType gpu1 is Android-only and requires contacting support to enable. Without support enablement, your runner type will revert to default.');
|
|
233
|
+
}
|
|
231
234
|
const additionalAppBinaryIds = nonFlatAdditionalAppBinaryIds?.flat();
|
|
232
235
|
const additionalAppFiles = nonFlatAdditionalAppFiles?.flat();
|
|
233
236
|
const { firstFile, secondFile } = args;
|
|
@@ -589,7 +592,7 @@ class Cloud extends core_1.Command {
|
|
|
589
592
|
uploadId: results[0].test_upload_id,
|
|
590
593
|
};
|
|
591
594
|
if (flags['json-file']) {
|
|
592
|
-
const jsonFilePath =
|
|
595
|
+
const jsonFilePath = jsonFileName || `${results[0].test_upload_id}_dcd.json`;
|
|
593
596
|
(0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput, this);
|
|
594
597
|
}
|
|
595
598
|
if (json) {
|
|
@@ -681,6 +684,10 @@ class Cloud extends core_1.Command {
|
|
|
681
684
|
this.warn('Failed to download artifacts');
|
|
682
685
|
}
|
|
683
686
|
}
|
|
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);
|
|
690
|
+
}
|
|
684
691
|
const resultsWithoutEarlierTries = updatedResults.filter((result) => {
|
|
685
692
|
const originalTryId = result.retry_of || result.id;
|
|
686
693
|
const tries = updatedResults.filter((r) => r.retry_of === originalTryId || r.id === originalTryId);
|
|
@@ -704,7 +711,7 @@ class Cloud extends core_1.Command {
|
|
|
704
711
|
uploadId: results[0].test_upload_id,
|
|
705
712
|
};
|
|
706
713
|
if (flags['json-file']) {
|
|
707
|
-
const jsonFilePath =
|
|
714
|
+
const jsonFilePath = jsonFileName || `${results[0].test_upload_id}_dcd.json`;
|
|
708
715
|
(0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput, this);
|
|
709
716
|
}
|
|
710
717
|
if (json) {
|
|
@@ -730,7 +737,7 @@ class Cloud extends core_1.Command {
|
|
|
730
737
|
uploadId: results[0].test_upload_id,
|
|
731
738
|
};
|
|
732
739
|
if (flags['json-file']) {
|
|
733
|
-
const jsonFilePath =
|
|
740
|
+
const jsonFilePath = jsonFileName || `${results[0].test_upload_id}_dcd.json`;
|
|
734
741
|
(0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput, this);
|
|
735
742
|
}
|
|
736
743
|
if (json) {
|
|
@@ -781,16 +788,62 @@ class Cloud extends core_1.Command {
|
|
|
781
788
|
}
|
|
782
789
|
}
|
|
783
790
|
/**
|
|
784
|
-
*
|
|
785
|
-
* @param
|
|
786
|
-
* @param
|
|
787
|
-
* @
|
|
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
|
|
788
799
|
*/
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
+
}
|
|
792
846
|
}
|
|
793
|
-
return `${uploadId}_dcd.json`;
|
|
794
847
|
}
|
|
795
848
|
}
|
|
796
849
|
exports.default = Cloud;
|
package/dist/constants.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export declare const flags: {
|
|
|
12
12
|
'app-binary-id': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
13
13
|
'app-file': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
14
14
|
'artifacts-path': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
15
|
+
'junit-path': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
15
16
|
async: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
16
17
|
config: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
17
18
|
debug: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
package/dist/constants.js
CHANGED
|
@@ -73,6 +73,10 @@ exports.flags = {
|
|
|
73
73
|
dependsOn: ['download-artifacts'],
|
|
74
74
|
description: 'Custom file path for downloaded artifacts (default: ./artifacts.zip)',
|
|
75
75
|
}),
|
|
76
|
+
'junit-path': core_1.Flags.string({
|
|
77
|
+
dependsOn: ['report'],
|
|
78
|
+
description: 'Custom file path for downloaded JUnit report (default: ./report.xml)',
|
|
79
|
+
}),
|
|
76
80
|
async: core_1.Flags.boolean({
|
|
77
81
|
description: 'Immediately return (exit code 0) from the command without waiting for the results of the run (useful for saving CI minutes)',
|
|
78
82
|
}),
|
|
@@ -87,7 +91,7 @@ exports.flags = {
|
|
|
87
91
|
description: 'Locale that will be set to a device, ISO-639-1 code and uppercase ISO-3166-1 code e.g. "de_DE" for Germany',
|
|
88
92
|
}),
|
|
89
93
|
'download-artifacts': core_1.Flags.string({
|
|
90
|
-
description: 'Download a zip containing the logs, screenshots and videos for each result in this run. You will debited a $0.01 egress fee for each result.
|
|
94
|
+
description: 'Download a zip containing the logs, screenshots and videos for each result in this run. You will be debited a $0.01 egress fee for each result. Options: ALL (everything), FAILED (failures only).',
|
|
91
95
|
options: ['ALL', 'FAILED'],
|
|
92
96
|
}),
|
|
93
97
|
'dry-run': core_1.Flags.boolean({
|
|
@@ -190,16 +194,16 @@ exports.flags = {
|
|
|
190
194
|
}),
|
|
191
195
|
report: core_1.Flags.string({
|
|
192
196
|
aliases: ['format'],
|
|
193
|
-
description: '
|
|
194
|
-
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'],
|
|
195
199
|
}),
|
|
196
200
|
retry: core_1.Flags.integer({
|
|
197
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',
|
|
198
202
|
}),
|
|
199
203
|
'runner-type': core_1.Flags.string({
|
|
200
204
|
default: 'default',
|
|
201
|
-
description: '[experimental] The type of runner to use - note: anything other than default will incur premium pricing tiers, see https://docs.devicecloud.dev/reference/runner-type for more information',
|
|
202
|
-
options: ['default', 'm4', 'm1'],
|
|
205
|
+
description: '[experimental] The type of runner to use - note: anything other than default will incur premium pricing tiers, see https://docs.devicecloud.dev/reference/runner-type for more information. gpu1 is Android-only and requires contacting support to enable, otherwise reverts to default.',
|
|
206
|
+
options: ['default', 'm4', 'm1', 'gpu1'],
|
|
203
207
|
}),
|
|
204
208
|
'show-crosshairs': core_1.Flags.boolean({
|
|
205
209
|
default: false,
|
|
@@ -1,5 +1,12 @@
|
|
|
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;
|
|
@@ -7,14 +14,14 @@ export declare const ApiGateway: {
|
|
|
7
14
|
downloadArtifactsZip(baseUrl: string, apiKey: string, uploadId: string, results: "ALL" | "FAILED", artifactsPath?: string): Promise<void>;
|
|
8
15
|
finaliseUpload(baseUrl: string, apiKey: string, id: string, metadata: TAppMetadata, path: string, sha: string): Promise<Record<string, never>>;
|
|
9
16
|
getBinaryUploadUrl(baseUrl: string, apiKey: string, platform: "android" | "ios"): Promise<{
|
|
10
|
-
id: string;
|
|
11
17
|
message: string;
|
|
12
18
|
path: string;
|
|
13
19
|
token: string;
|
|
20
|
+
id: string;
|
|
14
21
|
}>;
|
|
15
22
|
getResultsForUpload(baseUrl: string, apiKey: string, uploadId: string): Promise<{
|
|
16
|
-
results?: import("../types/schema.types").components["schemas"]["TResultResponse"][];
|
|
17
23
|
statusCode?: number;
|
|
24
|
+
results?: import("../types/schema.types").components["schemas"]["TResultResponse"][];
|
|
18
25
|
}>;
|
|
19
26
|
getUploadStatus(baseUrl: string, apiKey: string, options: {
|
|
20
27
|
name?: string;
|
|
@@ -32,4 +39,14 @@ export declare const ApiGateway: {
|
|
|
32
39
|
message?: string;
|
|
33
40
|
results?: import("../types/schema.types").components["schemas"]["IDBResult"][];
|
|
34
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>;
|
|
35
52
|
};
|
|
@@ -1,7 +1,50 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ApiGateway = void 0;
|
|
4
|
+
const path = require("node:path");
|
|
4
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
|
+
},
|
|
5
48
|
async checkForExistingUpload(baseUrl, apiKey, sha) {
|
|
6
49
|
const res = await fetch(`${baseUrl}/uploads/checkForExistingUpload`, {
|
|
7
50
|
body: JSON.stringify({ sha }),
|
|
@@ -23,7 +66,7 @@ exports.ApiGateway = {
|
|
|
23
66
|
method: 'POST',
|
|
24
67
|
});
|
|
25
68
|
if (!res.ok) {
|
|
26
|
-
|
|
69
|
+
await this.handleApiError(res, 'Failed to download artifacts');
|
|
27
70
|
}
|
|
28
71
|
// Handle tilde expansion for home directory
|
|
29
72
|
if (artifactsPath.startsWith('~/') || artifactsPath === '~') {
|
|
@@ -66,7 +109,7 @@ exports.ApiGateway = {
|
|
|
66
109
|
method: 'POST',
|
|
67
110
|
});
|
|
68
111
|
if (!res.ok) {
|
|
69
|
-
|
|
112
|
+
await this.handleApiError(res, 'Failed to finalize upload');
|
|
70
113
|
}
|
|
71
114
|
return res.json();
|
|
72
115
|
},
|
|
@@ -80,7 +123,7 @@ exports.ApiGateway = {
|
|
|
80
123
|
method: 'POST',
|
|
81
124
|
});
|
|
82
125
|
if (!res.ok) {
|
|
83
|
-
|
|
126
|
+
await this.handleApiError(res, 'Failed to get upload URL');
|
|
84
127
|
}
|
|
85
128
|
return res.json();
|
|
86
129
|
},
|
|
@@ -90,7 +133,7 @@ exports.ApiGateway = {
|
|
|
90
133
|
headers: { 'x-app-api-key': apiKey },
|
|
91
134
|
});
|
|
92
135
|
if (!res.ok) {
|
|
93
|
-
|
|
136
|
+
await this.handleApiError(res, 'Failed to get results');
|
|
94
137
|
}
|
|
95
138
|
return res.json();
|
|
96
139
|
},
|
|
@@ -108,7 +151,7 @@ exports.ApiGateway = {
|
|
|
108
151
|
},
|
|
109
152
|
});
|
|
110
153
|
if (!response.ok) {
|
|
111
|
-
|
|
154
|
+
await this.handleApiError(response, 'Failed to get upload status');
|
|
112
155
|
}
|
|
113
156
|
return response.json();
|
|
114
157
|
},
|
|
@@ -121,8 +164,88 @@ exports.ApiGateway = {
|
|
|
121
164
|
method: 'POST',
|
|
122
165
|
});
|
|
123
166
|
if (!res.ok) {
|
|
124
|
-
|
|
167
|
+
await this.handleApiError(res, 'Failed to upload test flows');
|
|
125
168
|
}
|
|
126
169
|
return res.json();
|
|
127
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
|
+
},
|
|
128
251
|
};
|