@devicecloud.dev/dcd 4.1.2 → 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 (46) hide show
  1. package/dist/commands/cloud.d.ts +26 -34
  2. package/dist/commands/cloud.js +117 -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/methods.d.ts +0 -4
  19. package/dist/methods.js +15 -80
  20. package/dist/services/device-validation.service.d.ts +29 -0
  21. package/dist/services/device-validation.service.js +72 -0
  22. package/dist/{plan.d.ts → services/execution-plan.service.d.ts} +1 -1
  23. package/dist/{plan.js → services/execution-plan.service.js} +10 -10
  24. package/dist/{planMethods.js → services/execution-plan.utils.js} +0 -1
  25. package/dist/services/metadata-extractor.service.d.ts +46 -0
  26. package/dist/services/metadata-extractor.service.js +138 -0
  27. package/dist/services/moropo.service.d.ts +20 -0
  28. package/dist/services/moropo.service.js +113 -0
  29. package/dist/services/report-download.service.d.ts +40 -0
  30. package/dist/services/report-download.service.js +110 -0
  31. package/dist/services/results-polling.service.d.ts +45 -0
  32. package/dist/services/results-polling.service.js +210 -0
  33. package/dist/services/test-submission.service.d.ts +41 -0
  34. package/dist/services/test-submission.service.js +116 -0
  35. package/dist/services/version.service.d.ts +31 -0
  36. package/dist/services/version.service.js +81 -0
  37. package/dist/types/{schema.types.d.ts → generated/schema.types.d.ts} +349 -349
  38. package/dist/types/index.d.ts +6 -0
  39. package/dist/types/index.js +24 -0
  40. package/dist/utils/compatibility.d.ts +5 -0
  41. package/oclif.manifest.json +195 -209
  42. package/package.json +2 -9
  43. /package/dist/{planMethods.d.ts → services/execution-plan.utils.d.ts} +0 -0
  44. /package/dist/types/{device.types.d.ts → domain/device.types.d.ts} +0 -0
  45. /package/dist/types/{device.types.js → domain/device.types.js} +0 -0
  46. /package/dist/types/{schema.types.js → generated/schema.types.js} +0 -0
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MoropoService = void 0;
4
+ const core_1 = require("@oclif/core");
5
+ const fs = require("node:fs");
6
+ const os = require("node:os");
7
+ const path = require("node:path");
8
+ const StreamZip = require("node-stream-zip");
9
+ /**
10
+ * Service for downloading and extracting Moropo tests from the Moropo API
11
+ */
12
+ class MoropoService {
13
+ MOROPO_API_URL = 'https://api.moropo.com/tests';
14
+ /**
15
+ * Download and extract Moropo tests from the API
16
+ * @param options Download configuration options
17
+ * @returns Path to the extracted Moropo tests directory
18
+ */
19
+ async downloadAndExtract(options) {
20
+ const { apiKey, branchName = 'main', debug = false, quiet = false, json = false, logger } = options;
21
+ if (debug && logger) {
22
+ logger('DEBUG: Moropo v1 API key detected, downloading tests from Moropo API');
23
+ logger(`DEBUG: Using branch name: ${branchName}`);
24
+ }
25
+ try {
26
+ if (!quiet && !json) {
27
+ core_1.ux.action.start('Downloading Moropo tests', 'Initializing', {
28
+ stdout: true,
29
+ });
30
+ }
31
+ const response = await fetch(this.MOROPO_API_URL, {
32
+ headers: {
33
+ accept: 'application/zip',
34
+ 'x-app-api-key': apiKey,
35
+ 'x-branch-name': branchName,
36
+ },
37
+ });
38
+ if (!response.ok) {
39
+ throw new Error(`Failed to download Moropo tests: ${response.statusText}`);
40
+ }
41
+ const contentLength = response.headers.get('content-length');
42
+ const totalSize = contentLength
43
+ ? Number.parseInt(contentLength, 10)
44
+ : 0;
45
+ let downloadedSize = 0;
46
+ const moropoDir = path.join(os.tmpdir(), `moropo-tests-${Date.now()}`);
47
+ if (debug && logger) {
48
+ logger(`DEBUG: Extracting Moropo tests to: ${moropoDir}`);
49
+ }
50
+ // Create moropo directory if it doesn't exist
51
+ if (!fs.existsSync(moropoDir)) {
52
+ fs.mkdirSync(moropoDir, { recursive: true });
53
+ }
54
+ // Write zip file to moropo directory
55
+ const zipPath = path.join(moropoDir, 'moropo-tests.zip');
56
+ const fileStream = fs.createWriteStream(zipPath);
57
+ const reader = response.body?.getReader();
58
+ if (!reader) {
59
+ throw new Error('Failed to get response reader');
60
+ }
61
+ let readerResult = await reader.read();
62
+ while (!readerResult.done) {
63
+ const { value } = readerResult;
64
+ downloadedSize += value.length;
65
+ if (!quiet && !json && totalSize) {
66
+ const progress = Math.round((downloadedSize / totalSize) * 100);
67
+ core_1.ux.action.status = `Downloading: ${progress}%`;
68
+ }
69
+ fileStream.write(value);
70
+ readerResult = await reader.read();
71
+ }
72
+ fileStream.end();
73
+ await new Promise((resolve) => {
74
+ fileStream.on('finish', () => {
75
+ resolve();
76
+ });
77
+ });
78
+ if (!quiet && !json) {
79
+ core_1.ux.action.status = 'Extracting tests...';
80
+ }
81
+ // Extract zip file
82
+ // eslint-disable-next-line new-cap
83
+ const zip = new StreamZip.async({ file: zipPath });
84
+ await zip.extract(null, moropoDir);
85
+ await zip.close();
86
+ // Delete zip file after extraction
87
+ fs.unlinkSync(zipPath);
88
+ if (!quiet && !json) {
89
+ core_1.ux.action.stop('completed');
90
+ }
91
+ if (debug && logger) {
92
+ logger('DEBUG: Successfully extracted Moropo tests');
93
+ }
94
+ // Create config.yaml file
95
+ const configPath = path.join(moropoDir, 'config.yaml');
96
+ fs.writeFileSync(configPath, 'flows:\n- ./**/*.yaml\n- ./*.yaml\n');
97
+ if (debug && logger) {
98
+ logger('DEBUG: Created config.yaml file');
99
+ }
100
+ return moropoDir;
101
+ }
102
+ catch (error) {
103
+ if (!quiet && !json) {
104
+ core_1.ux.action.stop('failed');
105
+ }
106
+ if (debug && logger) {
107
+ logger(`DEBUG: Error downloading/extracting Moropo tests: ${error}`);
108
+ }
109
+ throw new Error(`Failed to download/extract Moropo tests: ${error}`);
110
+ }
111
+ }
112
+ }
113
+ exports.MoropoService = MoropoService;
@@ -0,0 +1,40 @@
1
+ export interface DownloadOptions {
2
+ apiKey: string;
3
+ apiUrl: string;
4
+ debug?: boolean;
5
+ logger?: (message: string) => void;
6
+ uploadId: string;
7
+ warnLogger?: (message: string) => void;
8
+ }
9
+ export interface ArtifactsDownloadOptions extends DownloadOptions {
10
+ artifactsPath?: string;
11
+ downloadType: 'ALL' | 'FAILED';
12
+ }
13
+ export interface ReportDownloadOptions extends DownloadOptions {
14
+ allurePath?: string;
15
+ htmlPath?: string;
16
+ junitPath?: string;
17
+ reportType: 'allure' | 'html' | 'junit';
18
+ }
19
+ /**
20
+ * Service for downloading test artifacts and reports
21
+ */
22
+ export declare class ReportDownloadService {
23
+ /**
24
+ * Download test artifacts as a zip file
25
+ * @param options Download configuration
26
+ */
27
+ downloadArtifacts(options: ArtifactsDownloadOptions): Promise<void>;
28
+ /**
29
+ * Handle downloading reports based on the report type specified
30
+ * @param options Report download configuration
31
+ */
32
+ downloadReports(options: ReportDownloadOptions): Promise<void>;
33
+ /**
34
+ * Download a specific report type
35
+ * @param type Report type to download
36
+ * @param filePath Path where report should be saved
37
+ * @param options Download configuration
38
+ */
39
+ private downloadReport;
40
+ }
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ReportDownloadService = void 0;
4
+ const path = require("node:path");
5
+ const api_gateway_1 = require("../gateways/api-gateway");
6
+ /**
7
+ * Service for downloading test artifacts and reports
8
+ */
9
+ class ReportDownloadService {
10
+ /**
11
+ * Download test artifacts as a zip file
12
+ * @param options Download configuration
13
+ */
14
+ async downloadArtifacts(options) {
15
+ const { apiUrl, apiKey, uploadId, downloadType, artifactsPath = './artifacts.zip', debug = false, logger, warnLogger, } = options;
16
+ try {
17
+ if (debug && logger) {
18
+ logger(`DEBUG: Downloading artifacts: ${downloadType}`);
19
+ }
20
+ await api_gateway_1.ApiGateway.downloadArtifactsZip(apiUrl, apiKey, uploadId, downloadType, artifactsPath);
21
+ if (logger) {
22
+ logger('\n');
23
+ logger(`Test artifacts have been downloaded to ${artifactsPath}`);
24
+ }
25
+ }
26
+ catch (error) {
27
+ if (debug && logger) {
28
+ logger(`DEBUG: Error downloading artifacts: ${error}`);
29
+ }
30
+ if (warnLogger) {
31
+ warnLogger('Failed to download artifacts');
32
+ }
33
+ }
34
+ }
35
+ /**
36
+ * Handle downloading reports based on the report type specified
37
+ * @param options Report download configuration
38
+ */
39
+ async downloadReports(options) {
40
+ const { reportType, junitPath, allurePath, htmlPath, warnLogger, ...downloadOptions } = options;
41
+ switch (reportType) {
42
+ case 'junit': {
43
+ const reportPath = path.resolve(process.cwd(), junitPath || 'report.xml');
44
+ await this.downloadReport('junit', reportPath, {
45
+ ...downloadOptions,
46
+ warnLogger,
47
+ });
48
+ break;
49
+ }
50
+ case 'allure': {
51
+ const reportPath = path.resolve(process.cwd(), allurePath || 'report.html');
52
+ await this.downloadReport('allure', reportPath, {
53
+ ...downloadOptions,
54
+ warnLogger,
55
+ });
56
+ break;
57
+ }
58
+ case 'html': {
59
+ const htmlReportPath = path.resolve(process.cwd(), htmlPath || 'report.html');
60
+ await this.downloadReport('html', htmlReportPath, {
61
+ ...downloadOptions,
62
+ warnLogger,
63
+ });
64
+ break;
65
+ }
66
+ default: {
67
+ if (warnLogger) {
68
+ warnLogger(`Unknown report type: ${reportType}`);
69
+ }
70
+ }
71
+ }
72
+ }
73
+ /**
74
+ * Download a specific report type
75
+ * @param type Report type to download
76
+ * @param filePath Path where report should be saved
77
+ * @param options Download configuration
78
+ */
79
+ async downloadReport(type, filePath, options) {
80
+ const { apiUrl, apiKey, uploadId, debug = false, logger, warnLogger } = options;
81
+ try {
82
+ if (debug && logger) {
83
+ logger(`DEBUG: Downloading ${type.toUpperCase()} report`);
84
+ }
85
+ await api_gateway_1.ApiGateway.downloadReportGeneric(apiUrl, apiKey, uploadId, type, filePath);
86
+ if (logger) {
87
+ logger(`${type.toUpperCase()} test report has been downloaded to ${filePath}`);
88
+ }
89
+ }
90
+ catch (error) {
91
+ if (debug && logger) {
92
+ logger(`DEBUG: Error downloading ${type.toUpperCase()} report: ${error}`);
93
+ }
94
+ const errorMessage = error instanceof Error ? error.message : String(error);
95
+ if (warnLogger) {
96
+ warnLogger(`Failed to download ${type.toUpperCase()} report: ${errorMessage}`);
97
+ if (errorMessage.includes('404')) {
98
+ warnLogger(`No ${type.toUpperCase()} reports found for this upload. Make sure your tests generated results.`);
99
+ }
100
+ else if (errorMessage.includes('EACCES') || errorMessage.includes('EPERM')) {
101
+ warnLogger('Permission denied. Check write permissions for the current directory.');
102
+ }
103
+ else if (errorMessage.includes('ENOENT')) {
104
+ warnLogger('Directory does not exist. Make sure you have write access to the current directory.');
105
+ }
106
+ }
107
+ }
108
+ }
109
+ }
110
+ exports.ReportDownloadService = ReportDownloadService;
@@ -0,0 +1,45 @@
1
+ import { paths } from '../types/generated/schema.types';
2
+ type TestResult = paths['/results/{uploadId}']['get']['responses']['200']['content']['application/json']['results'][number];
3
+ /**
4
+ * Custom error for run failures that includes the polling result
5
+ */
6
+ export declare class RunFailedError extends Error {
7
+ result: PollingResult;
8
+ constructor(result: PollingResult);
9
+ }
10
+ export interface PollingOptions {
11
+ apiKey: string;
12
+ apiUrl: string;
13
+ consoleUrl: string;
14
+ debug?: boolean;
15
+ json?: boolean;
16
+ logger?: (message: string) => void;
17
+ quiet?: boolean;
18
+ uploadId: string;
19
+ }
20
+ export interface PollingResult {
21
+ consoleUrl: string;
22
+ status: 'FAILED' | 'PASSED';
23
+ tests: Array<{
24
+ durationSeconds: null | number;
25
+ failReason?: string;
26
+ name: string;
27
+ status: string;
28
+ }>;
29
+ uploadId: string;
30
+ }
31
+ /**
32
+ * Service for polling test results from the API
33
+ */
34
+ export declare class ResultsPollingService {
35
+ private readonly MAX_SEQUENTIAL_FAILURES;
36
+ private readonly POLL_INTERVAL_MS;
37
+ /**
38
+ * Poll for test results until all tests complete
39
+ * @param results Initial test results from submission
40
+ * @param options Polling configuration
41
+ * @returns Promise that resolves with final test results or rejects if tests fail
42
+ */
43
+ pollUntilComplete(results: TestResult[], options: PollingOptions): Promise<PollingResult>;
44
+ }
45
+ export {};
@@ -0,0 +1,210 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ResultsPollingService = exports.RunFailedError = void 0;
4
+ const core_1 = require("@oclif/core");
5
+ const cli_ux_1 = require("@oclif/core/lib/cli-ux");
6
+ const api_gateway_1 = require("../gateways/api-gateway");
7
+ const methods_1 = require("../methods");
8
+ const connectivity_1 = require("../utils/connectivity");
9
+ /**
10
+ * Custom error for run failures that includes the polling result
11
+ */
12
+ class RunFailedError extends Error {
13
+ result;
14
+ constructor(result) {
15
+ super('RUN_FAILED');
16
+ this.result = result;
17
+ this.name = 'RunFailedError';
18
+ }
19
+ }
20
+ exports.RunFailedError = RunFailedError;
21
+ /**
22
+ * Service for polling test results from the API
23
+ */
24
+ class ResultsPollingService {
25
+ MAX_SEQUENTIAL_FAILURES = 10;
26
+ POLL_INTERVAL_MS = 5000;
27
+ /**
28
+ * Poll for test results until all tests complete
29
+ * @param results Initial test results from submission
30
+ * @param options Polling configuration
31
+ * @returns Promise that resolves with final test results or rejects if tests fail
32
+ */
33
+ async pollUntilComplete(results, options) {
34
+ const { apiUrl, apiKey, uploadId, consoleUrl, quiet = false, json = false, debug = false, logger } = options;
35
+ if (!json) {
36
+ core_1.ux.action.start('Waiting for results', 'Initializing', {
37
+ stdout: true,
38
+ });
39
+ if (logger) {
40
+ logger('\nYou can safely close this terminal and the tests will continue\n');
41
+ }
42
+ }
43
+ let sequentialPollFailures = 0;
44
+ let previousSummary = '';
45
+ if (debug && logger) {
46
+ logger(`DEBUG: Starting polling loop for results`);
47
+ }
48
+ return new Promise((resolve, reject) => {
49
+ const intervalId = setInterval(async () => {
50
+ try {
51
+ if (debug && logger) {
52
+ logger(`DEBUG: Polling for results: ${uploadId}`);
53
+ }
54
+ const { results: updatedResults } = await api_gateway_1.ApiGateway.getResultsForUpload(apiUrl, apiKey, uploadId);
55
+ if (!updatedResults) {
56
+ throw new Error('no results');
57
+ }
58
+ if (debug && logger) {
59
+ logger(`DEBUG: Poll received ${updatedResults.length} results`);
60
+ for (const result of updatedResults) {
61
+ logger(`DEBUG: Result status: ${result.test_file_name} - ${result.status}`);
62
+ }
63
+ }
64
+ // Calculate summary statistics
65
+ const statusCounts = {};
66
+ for (const result of updatedResults) {
67
+ statusCounts[result.status] = (statusCounts[result.status] || 0) + 1;
68
+ }
69
+ const passed = statusCounts.PASSED || 0;
70
+ const failed = statusCounts.FAILED || 0;
71
+ const pending = statusCounts.PENDING || 0;
72
+ const running = statusCounts.RUNNING || 0;
73
+ const total = updatedResults.length;
74
+ const completed = passed + failed;
75
+ // Display quantitative summary in quiet mode only
76
+ const summary = `${completed}/${total} | ✓ ${passed} | ✗ ${failed} | ▶ ${running} | ⏸ ${pending}`;
77
+ if (!json) {
78
+ if (quiet) {
79
+ // Only update status when the summary changes in quiet mode
80
+ if (summary !== previousSummary) {
81
+ core_1.ux.action.status = summary;
82
+ previousSummary = summary;
83
+ }
84
+ }
85
+ else {
86
+ // Show detailed table in non-quiet mode (no summary)
87
+ core_1.ux.action.status =
88
+ '\nStatus Test\n─────────── ───────────────';
89
+ for (const { retry_of: isRetry, status, test_file_name: test, } of updatedResults) {
90
+ core_1.ux.action.status += `\n${status.padEnd(10, ' ')} ${test} ${isRetry ? '(retry)' : ''}`;
91
+ }
92
+ }
93
+ }
94
+ if (updatedResults.every((result) => !['PENDING', 'RUNNING'].includes(result.status))) {
95
+ if (debug && logger) {
96
+ logger(`DEBUG: All tests completed, stopping poll`);
97
+ }
98
+ if (!json) {
99
+ core_1.ux.action.stop('completed');
100
+ if (logger) {
101
+ logger('\n');
102
+ }
103
+ const hasFailedTests = updatedResults.some((result) => result.status === 'FAILED');
104
+ (0, cli_ux_1.table)(updatedResults, {
105
+ status: { get: (row) => row.status },
106
+ test: {
107
+ get: (row) => `${row.test_file_name} ${row.retry_of ? '(retry)' : ''}`,
108
+ },
109
+ duration: {
110
+ get: (row) => row.duration_seconds
111
+ ? (0, methods_1.formatDurationSeconds)(Number(row.duration_seconds))
112
+ : '-',
113
+ },
114
+ ...(hasFailedTests && {
115
+ // eslint-disable-next-line camelcase
116
+ fail_reason: {
117
+ get: (row) => row.status === 'FAILED' && row.fail_reason
118
+ ? row.fail_reason
119
+ : '',
120
+ },
121
+ }),
122
+ }, { printLine: logger });
123
+ if (logger) {
124
+ logger('\n');
125
+ logger('Run completed, you can access the results at:');
126
+ logger(consoleUrl);
127
+ logger('\n');
128
+ }
129
+ }
130
+ clearInterval(intervalId);
131
+ const resultsWithoutEarlierTries = updatedResults.filter((result) => {
132
+ const originalTryId = result.retry_of || result.id;
133
+ const tries = updatedResults.filter((r) => r.retry_of === originalTryId || r.id === originalTryId);
134
+ return result.id === Math.max(...tries.map((t) => t.id));
135
+ });
136
+ const output = {
137
+ consoleUrl,
138
+ status: resultsWithoutEarlierTries.some((result) => result.status === 'FAILED')
139
+ ? 'FAILED'
140
+ : 'PASSED',
141
+ tests: resultsWithoutEarlierTries.map((r) => ({
142
+ durationSeconds: r.duration_seconds,
143
+ failReason: r.status === 'FAILED'
144
+ ? r.fail_reason || 'No reason provided'
145
+ : undefined,
146
+ name: r.test_file_name,
147
+ status: r.status,
148
+ })),
149
+ uploadId,
150
+ };
151
+ if (output.status === 'FAILED') {
152
+ if (debug && logger) {
153
+ logger(`DEBUG: Some tests failed, returning failed status`);
154
+ }
155
+ reject(new RunFailedError(output));
156
+ }
157
+ else {
158
+ if (debug && logger) {
159
+ logger(`DEBUG: All tests passed, returning success status`);
160
+ }
161
+ sequentialPollFailures = 0;
162
+ resolve(output);
163
+ }
164
+ }
165
+ }
166
+ catch (error) {
167
+ sequentialPollFailures++;
168
+ if (debug && logger) {
169
+ logger(`DEBUG: Error polling for results: ${error}`);
170
+ logger(`DEBUG: Sequential poll failures: ${sequentialPollFailures}`);
171
+ }
172
+ if (sequentialPollFailures > this.MAX_SEQUENTIAL_FAILURES) {
173
+ // dropped poll requests shouldn't err user CI
174
+ clearInterval(intervalId);
175
+ // Check if the failure is due to internet connectivity issues
176
+ if (debug && logger) {
177
+ logger('DEBUG: Checking internet connectivity...');
178
+ }
179
+ const connectivityCheck = await (0, connectivity_1.checkInternetConnectivity)();
180
+ if (debug && logger) {
181
+ logger(`DEBUG: ${connectivityCheck.message}`);
182
+ for (const result of connectivityCheck.endpointResults) {
183
+ if (result.success) {
184
+ logger(`DEBUG: ✓ ${result.endpoint} - ${result.statusCode} (${result.latencyMs}ms)`);
185
+ }
186
+ else {
187
+ logger(`DEBUG: ✗ ${result.endpoint} - ${result.error} (${result.latencyMs}ms)`);
188
+ }
189
+ }
190
+ }
191
+ if (!connectivityCheck.connected) {
192
+ // Build detailed error message with endpoint diagnostics
193
+ const endpointDetails = connectivityCheck.endpointResults
194
+ .map((r) => ` - ${r.endpoint}: ${r.error} (${r.latencyMs}ms)`)
195
+ .join('\n');
196
+ reject(new Error(`Unable to fetch results after ${this.MAX_SEQUENTIAL_FAILURES} attempts.\n\nInternet connectivity check failed - all test endpoints unreachable:\n${endpointDetails}\n\nPlease verify your network connection and DNS resolution.`));
197
+ return;
198
+ }
199
+ reject(new Error(`unable to fetch results after ${this.MAX_SEQUENTIAL_FAILURES} attempts`));
200
+ return;
201
+ }
202
+ if (logger) {
203
+ logger('unable to fetch results, trying again...');
204
+ }
205
+ }
206
+ }, this.POLL_INTERVAL_MS);
207
+ });
208
+ }
209
+ }
210
+ exports.ResultsPollingService = ResultsPollingService;
@@ -0,0 +1,41 @@
1
+ import { IExecutionPlan } from './execution-plan.service';
2
+ export interface TestSubmissionConfig {
3
+ androidApiLevel?: string;
4
+ androidDevice?: string;
5
+ appBinaryId: string;
6
+ cliVersion: string;
7
+ commonRoot: string;
8
+ continueOnFailure?: boolean;
9
+ debug?: boolean;
10
+ deviceLocale?: string;
11
+ env?: string[];
12
+ executionPlan: IExecutionPlan;
13
+ flowFile: string;
14
+ googlePlay?: boolean;
15
+ iOSDevice?: string;
16
+ iOSVersion?: string;
17
+ logger?: (message: string) => void;
18
+ maestroVersion: string;
19
+ metadata?: string[];
20
+ mitmHost?: string;
21
+ mitmPath?: string;
22
+ name?: string;
23
+ orientation?: string;
24
+ raw?: unknown;
25
+ report?: string;
26
+ retry?: number;
27
+ runnerType?: string;
28
+ showCrosshairs?: boolean;
29
+ skipChromeOnboarding?: boolean;
30
+ }
31
+ /**
32
+ * Service for building test submission form data
33
+ */
34
+ export declare class TestSubmissionService {
35
+ /**
36
+ * Build FormData for test submission
37
+ * @param config Test submission configuration
38
+ * @returns FormData ready to be submitted to the API
39
+ */
40
+ buildTestFormData(config: TestSubmissionConfig): Promise<FormData>;
41
+ }
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TestSubmissionService = void 0;
4
+ const path = require("node:path");
5
+ const methods_1 = require("../methods");
6
+ const mimeTypeLookupByExtension = {
7
+ zip: 'application/zip',
8
+ };
9
+ /**
10
+ * Service for building test submission form data
11
+ */
12
+ class TestSubmissionService {
13
+ /**
14
+ * Build FormData for test submission
15
+ * @param config Test submission configuration
16
+ * @returns FormData ready to be submitted to the API
17
+ */
18
+ async buildTestFormData(config) {
19
+ const { appBinaryId, flowFile, executionPlan, commonRoot, cliVersion, env = [], metadata = [], googlePlay = false, androidApiLevel, androidDevice, iOSVersion, iOSDevice, name, runnerType, maestroVersion, deviceLocale, orientation, mitmHost, mitmPath, retry, continueOnFailure = true, report, showCrosshairs, skipChromeOnboarding, raw, debug = false, logger, } = config;
20
+ const { allExcludeTags, allIncludeTags, flowMetadata, flowOverrides, flowsToRun: testFileNames, referencedFiles, sequence, workspaceConfig, } = executionPlan;
21
+ const { flows: sequentialFlows = [] } = sequence ?? {};
22
+ const testFormData = new FormData();
23
+ // eslint-disable-next-line unicorn/no-array-reduce
24
+ const envObject = env.reduce((acc, cur) => {
25
+ const [key, ...value] = cur.split('=');
26
+ // handle case where value includes an equals sign
27
+ acc[key] = value.join('=');
28
+ return acc;
29
+ }, {});
30
+ // eslint-disable-next-line unicorn/no-array-reduce
31
+ const metadataObject = metadata.reduce((acc, cur) => {
32
+ const [key, ...value] = cur.split('=');
33
+ // handle case where value includes an equals sign
34
+ acc[key] = value.join('=');
35
+ return acc;
36
+ }, {});
37
+ if (debug && logger && Object.keys(envObject).length > 0) {
38
+ logger(`DEBUG: Environment variables: ${JSON.stringify(envObject)}`);
39
+ }
40
+ if (debug && logger && Object.keys(metadataObject).length > 0) {
41
+ logger(`DEBUG: User metadata: ${JSON.stringify(metadataObject)}`);
42
+ }
43
+ if (debug && logger) {
44
+ logger(`DEBUG: Compressing files from path: ${flowFile}`);
45
+ }
46
+ const buffer = await (0, methods_1.compressFilesFromRelativePath)(flowFile?.endsWith('.yaml') || flowFile?.endsWith('.yml')
47
+ ? path.dirname(flowFile)
48
+ : flowFile, [
49
+ ...new Set([
50
+ ...referencedFiles,
51
+ ...testFileNames,
52
+ ...sequentialFlows,
53
+ ]),
54
+ ], commonRoot);
55
+ if (debug && logger) {
56
+ logger(`DEBUG: Compressed file size: ${buffer.length} bytes`);
57
+ }
58
+ const blob = new Blob([buffer], {
59
+ type: mimeTypeLookupByExtension.zip,
60
+ });
61
+ testFormData.set('file', blob, 'flowFile.zip');
62
+ testFormData.set('appBinaryId', appBinaryId);
63
+ testFormData.set('testFileNames', JSON.stringify(testFileNames.map((t) => t.replaceAll(commonRoot, '.').split(path.sep).join('/'))));
64
+ testFormData.set('flowMetadata', JSON.stringify(Object.fromEntries(Object.entries(flowMetadata).map(([key, value]) => [
65
+ key.replaceAll(commonRoot, '.').split(path.sep).join('/'),
66
+ value,
67
+ ]))));
68
+ testFormData.set('testFileOverrides', JSON.stringify(Object.fromEntries(Object.entries(flowOverrides).map(([key, value]) => [
69
+ key.replaceAll(commonRoot, '.').split(path.sep).join('/'),
70
+ value,
71
+ ]))));
72
+ testFormData.set('sequentialFlows', JSON.stringify(sequentialFlows.map((t) => t.replaceAll(commonRoot, '.').split(path.sep).join('/'))));
73
+ testFormData.set('env', JSON.stringify(envObject));
74
+ testFormData.set('googlePlay', googlePlay ? 'true' : 'false');
75
+ const configPayload = {
76
+ allExcludeTags,
77
+ allIncludeTags,
78
+ autoRetriesRemaining: retry,
79
+ continueOnFailure,
80
+ deviceLocale,
81
+ maestroVersion,
82
+ mitmHost,
83
+ mitmPath,
84
+ orientation,
85
+ raw: JSON.stringify(raw),
86
+ report,
87
+ showCrosshairs,
88
+ skipChromeOnboarding,
89
+ version: cliVersion,
90
+ };
91
+ testFormData.set('config', JSON.stringify(configPayload));
92
+ if (Object.keys(metadataObject).length > 0) {
93
+ const metadataPayload = { userMetadata: metadataObject };
94
+ testFormData.set('metadata', JSON.stringify(metadataPayload));
95
+ if (debug && logger) {
96
+ logger(`DEBUG: Sending metadata to API: ${JSON.stringify(metadataPayload)}`);
97
+ }
98
+ }
99
+ if (androidApiLevel)
100
+ testFormData.set('androidApiLevel', androidApiLevel.toString());
101
+ if (androidDevice)
102
+ testFormData.set('androidDevice', androidDevice.toString());
103
+ if (iOSVersion)
104
+ testFormData.set('iOSVersion', iOSVersion.toString());
105
+ if (iOSDevice)
106
+ testFormData.set('iOSDevice', iOSDevice.toString());
107
+ if (name)
108
+ testFormData.set('name', name.toString());
109
+ if (runnerType)
110
+ testFormData.set('runnerType', runnerType.toString());
111
+ if (workspaceConfig)
112
+ testFormData.set('workspaceConfig', JSON.stringify(workspaceConfig));
113
+ return testFormData;
114
+ }
115
+ }
116
+ exports.TestSubmissionService = TestSubmissionService;