@devicecloud.dev/dcd 4.1.3 → 4.1.5

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.
@@ -61,93 +61,16 @@ class ResultsPollingService {
61
61
  logger(`DEBUG: Result status: ${result.test_file_name} - ${result.status}`);
62
62
  }
63
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))) {
64
+ const { summary } = this.calculateStatusSummary(updatedResults);
65
+ previousSummary = this.updateDisplayStatus(updatedResults, quiet, json, summary, previousSummary);
66
+ const allComplete = updatedResults.every((result) => !['PENDING', 'RUNNING'].includes(result.status));
67
+ if (allComplete) {
95
68
  if (debug && logger) {
96
69
  logger(`DEBUG: All tests completed, stopping poll`);
97
70
  }
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
- }
71
+ this.displayFinalResults(updatedResults, consoleUrl, json, logger);
130
72
  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
- };
73
+ const output = this.buildPollingResult(updatedResults, uploadId, consoleUrl);
151
74
  if (output.status === 'FAILED') {
152
75
  if (debug && logger) {
153
76
  logger(`DEBUG: Some tests failed, returning failed status`);
@@ -165,46 +88,137 @@ class ResultsPollingService {
165
88
  }
166
89
  catch (error) {
167
90
  sequentialPollFailures++;
168
- if (debug && logger) {
169
- logger(`DEBUG: Error polling for results: ${error}`);
170
- logger(`DEBUG: Sequential poll failures: ${sequentialPollFailures}`);
91
+ try {
92
+ await this.handlePollingError(error, sequentialPollFailures, debug, logger);
171
93
  }
172
- if (sequentialPollFailures > this.MAX_SEQUENTIAL_FAILURES) {
173
- // dropped poll requests shouldn't err user CI
94
+ catch (error) {
174
95
  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...');
96
+ reject(error);
204
97
  }
205
98
  }
206
99
  }, this.POLL_INTERVAL_MS);
207
100
  });
208
101
  }
102
+ buildPollingResult(results, uploadId, consoleUrl) {
103
+ const resultsWithoutEarlierTries = this.filterLatestResults(results);
104
+ return {
105
+ consoleUrl,
106
+ status: resultsWithoutEarlierTries.some((result) => result.status === 'FAILED')
107
+ ? 'FAILED'
108
+ : 'PASSED',
109
+ tests: resultsWithoutEarlierTries.map((r) => ({
110
+ durationSeconds: r.duration_seconds,
111
+ failReason: r.status === 'FAILED' ? r.fail_reason || 'No reason provided' : undefined,
112
+ name: r.test_file_name,
113
+ status: r.status,
114
+ })),
115
+ uploadId,
116
+ };
117
+ }
118
+ calculateStatusSummary(results) {
119
+ const statusCounts = {};
120
+ for (const result of results) {
121
+ statusCounts[result.status] = (statusCounts[result.status] || 0) + 1;
122
+ }
123
+ const passed = statusCounts.PASSED || 0;
124
+ const failed = statusCounts.FAILED || 0;
125
+ const pending = statusCounts.PENDING || 0;
126
+ const running = statusCounts.RUNNING || 0;
127
+ const total = results.length;
128
+ const completed = passed + failed;
129
+ const summary = `${completed}/${total} | ✓ ${passed} | ✗ ${failed} | ▶ ${running} | ⏸ ${pending}`;
130
+ return { completed, failed, passed, pending, running, summary, total };
131
+ }
132
+ displayFinalResults(results, consoleUrl, json, logger) {
133
+ if (json) {
134
+ return;
135
+ }
136
+ core_1.ux.action.stop('completed');
137
+ if (logger) {
138
+ logger('\n');
139
+ }
140
+ const hasFailedTests = results.some((result) => result.status === 'FAILED');
141
+ (0, cli_ux_1.table)(results, {
142
+ duration: {
143
+ get: (row) => row.duration_seconds
144
+ ? (0, methods_1.formatDurationSeconds)(Number(row.duration_seconds))
145
+ : '-',
146
+ },
147
+ status: { get: (row) => row.status },
148
+ test: {
149
+ get: (row) => `${row.test_file_name} ${row.retry_of ? '(retry)' : ''}`,
150
+ },
151
+ ...(hasFailedTests && {
152
+ // eslint-disable-next-line camelcase
153
+ fail_reason: {
154
+ get: (row) => row.status === 'FAILED' && row.fail_reason ? row.fail_reason : '',
155
+ },
156
+ }),
157
+ }, { printLine: logger });
158
+ if (logger) {
159
+ logger('\n');
160
+ logger('Run completed, you can access the results at:');
161
+ logger(consoleUrl);
162
+ logger('\n');
163
+ }
164
+ }
165
+ filterLatestResults(results) {
166
+ return results.filter((result) => {
167
+ const originalTryId = result.retry_of || result.id;
168
+ const tries = results.filter((r) => r.retry_of === originalTryId || r.id === originalTryId);
169
+ return result.id === Math.max(...tries.map((t) => t.id));
170
+ });
171
+ }
172
+ async handlePollingError(error, sequentialPollFailures, debug, logger) {
173
+ if (debug && logger) {
174
+ logger(`DEBUG: Error polling for results: ${error}`);
175
+ logger(`DEBUG: Sequential poll failures: ${sequentialPollFailures}`);
176
+ }
177
+ if (sequentialPollFailures > this.MAX_SEQUENTIAL_FAILURES) {
178
+ if (debug && logger) {
179
+ logger('DEBUG: Checking internet connectivity...');
180
+ }
181
+ const connectivityCheck = await (0, connectivity_1.checkInternetConnectivity)();
182
+ if (debug && logger) {
183
+ logger(`DEBUG: ${connectivityCheck.message}`);
184
+ for (const result of connectivityCheck.endpointResults) {
185
+ if (result.success) {
186
+ logger(`DEBUG: ✓ ${result.endpoint} - ${result.statusCode} (${result.latencyMs}ms)`);
187
+ }
188
+ else {
189
+ logger(`DEBUG: ✗ ${result.endpoint} - ${result.error} (${result.latencyMs}ms)`);
190
+ }
191
+ }
192
+ }
193
+ if (!connectivityCheck.connected) {
194
+ const endpointDetails = connectivityCheck.endpointResults
195
+ .map((r) => ` - ${r.endpoint}: ${r.error} (${r.latencyMs}ms)`)
196
+ .join('\n');
197
+ throw 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.`);
198
+ }
199
+ throw new Error(`unable to fetch results after ${this.MAX_SEQUENTIAL_FAILURES} attempts`);
200
+ }
201
+ if (logger) {
202
+ logger('unable to fetch results, trying again...');
203
+ }
204
+ }
205
+ updateDisplayStatus(results, quiet, json, summary, previousSummary) {
206
+ if (json) {
207
+ return previousSummary;
208
+ }
209
+ if (quiet) {
210
+ if (summary !== previousSummary) {
211
+ core_1.ux.action.status = summary;
212
+ return summary;
213
+ }
214
+ }
215
+ else {
216
+ core_1.ux.action.status = '\nStatus Test\n─────────── ───────────────';
217
+ for (const { retry_of: isRetry, status, test_file_name: test, } of results) {
218
+ core_1.ux.action.status += `\n${status.padEnd(10, ' ')} ${test} ${isRetry ? '(retry)' : ''}`;
219
+ }
220
+ }
221
+ return previousSummary;
222
+ }
209
223
  }
210
224
  exports.ResultsPollingService = ResultsPollingService;
@@ -38,4 +38,10 @@ export declare class TestSubmissionService {
38
38
  * @returns FormData ready to be submitted to the API
39
39
  */
40
40
  buildTestFormData(config: TestSubmissionConfig): Promise<FormData>;
41
+ private logDebug;
42
+ private normalizeFilePath;
43
+ private normalizePathMap;
44
+ private normalizePaths;
45
+ private parseKeyValuePairs;
46
+ private setOptionalFields;
41
47
  }
@@ -20,29 +20,15 @@ class TestSubmissionService {
20
20
  const { allExcludeTags, allIncludeTags, flowMetadata, flowOverrides, flowsToRun: testFileNames, referencedFiles, sequence, workspaceConfig, } = executionPlan;
21
21
  const { flows: sequentialFlows = [] } = sequence ?? {};
22
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)}`);
23
+ const envObject = this.parseKeyValuePairs(env);
24
+ const metadataObject = this.parseKeyValuePairs(metadata);
25
+ if (Object.keys(envObject).length > 0) {
26
+ this.logDebug(debug, logger, `DEBUG: Environment variables: ${JSON.stringify(envObject)}`);
42
27
  }
43
- if (debug && logger) {
44
- logger(`DEBUG: Compressing files from path: ${flowFile}`);
28
+ if (Object.keys(metadataObject).length > 0) {
29
+ this.logDebug(debug, logger, `DEBUG: User metadata: ${JSON.stringify(metadataObject)}`);
45
30
  }
31
+ this.logDebug(debug, logger, `DEBUG: Compressing files from path: ${flowFile}`);
46
32
  const buffer = await (0, methods_1.compressFilesFromRelativePath)(flowFile?.endsWith('.yaml') || flowFile?.endsWith('.yml')
47
33
  ? path.dirname(flowFile)
48
34
  : flowFile, [
@@ -52,24 +38,16 @@ class TestSubmissionService {
52
38
  ...sequentialFlows,
53
39
  ]),
54
40
  ], commonRoot);
55
- if (debug && logger) {
56
- logger(`DEBUG: Compressed file size: ${buffer.length} bytes`);
57
- }
41
+ this.logDebug(debug, logger, `DEBUG: Compressed file size: ${buffer.length} bytes`);
58
42
  const blob = new Blob([buffer], {
59
43
  type: mimeTypeLookupByExtension.zip,
60
44
  });
61
45
  testFormData.set('file', blob, 'flowFile.zip');
62
46
  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('/'))));
47
+ testFormData.set('testFileNames', JSON.stringify(this.normalizePaths(testFileNames, commonRoot)));
48
+ testFormData.set('flowMetadata', JSON.stringify(this.normalizePathMap(flowMetadata, commonRoot)));
49
+ testFormData.set('testFileOverrides', JSON.stringify(this.normalizePathMap(flowOverrides, commonRoot)));
50
+ testFormData.set('sequentialFlows', JSON.stringify(this.normalizePaths(sequentialFlows, commonRoot)));
73
51
  testFormData.set('env', JSON.stringify(envObject));
74
52
  testFormData.set('googlePlay', googlePlay ? 'true' : 'false');
75
53
  const configPayload = {
@@ -92,25 +70,53 @@ class TestSubmissionService {
92
70
  if (Object.keys(metadataObject).length > 0) {
93
71
  const metadataPayload = { userMetadata: metadataObject };
94
72
  testFormData.set('metadata', JSON.stringify(metadataPayload));
95
- if (debug && logger) {
96
- logger(`DEBUG: Sending metadata to API: ${JSON.stringify(metadataPayload)}`);
97
- }
73
+ this.logDebug(debug, logger, `DEBUG: Sending metadata to API: ${JSON.stringify(metadataPayload)}`);
98
74
  }
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)
75
+ this.setOptionalFields(testFormData, {
76
+ androidApiLevel,
77
+ androidDevice,
78
+ iOSDevice,
79
+ iOSVersion,
80
+ name,
81
+ runnerType,
82
+ });
83
+ if (workspaceConfig) {
112
84
  testFormData.set('workspaceConfig', JSON.stringify(workspaceConfig));
85
+ }
113
86
  return testFormData;
114
87
  }
88
+ logDebug(debug, logger, message) {
89
+ if (debug && logger) {
90
+ logger(message);
91
+ }
92
+ }
93
+ normalizeFilePath(filePath, commonRoot) {
94
+ return filePath.replaceAll(commonRoot, '.').split(path.sep).join('/');
95
+ }
96
+ normalizePathMap(map, commonRoot) {
97
+ return Object.fromEntries(Object.entries(map).map(([key, value]) => [
98
+ this.normalizeFilePath(key, commonRoot),
99
+ value,
100
+ ]));
101
+ }
102
+ normalizePaths(paths, commonRoot) {
103
+ return paths.map((p) => this.normalizeFilePath(p, commonRoot));
104
+ }
105
+ parseKeyValuePairs(pairs) {
106
+ // eslint-disable-next-line unicorn/no-array-reduce
107
+ return pairs.reduce((acc, cur) => {
108
+ const [key, ...value] = cur.split('=');
109
+ // handle case where value includes an equals sign
110
+ acc[key] = value.join('=');
111
+ return acc;
112
+ }, {});
113
+ }
114
+ setOptionalFields(formData, fields) {
115
+ for (const [key, value] of Object.entries(fields)) {
116
+ if (value) {
117
+ formData.set(key, value.toString());
118
+ }
119
+ }
120
+ }
115
121
  }
116
122
  exports.TestSubmissionService = TestSubmissionService;
@@ -19,8 +19,9 @@ export declare class VersionService {
19
19
  * Resolve and validate Maestro version against API compatibility data
20
20
  * @param requestedVersion - Version requested by user (or undefined for default)
21
21
  * @param compatibilityData - API compatibility data
22
- * @param debug - Enable debug logging
23
- * @param logger - Optional logger function
22
+ * @param options - Configuration options
23
+ * @param options.debug - Enable debug logging
24
+ * @param options.logger - Optional logger function
24
25
  * @returns Validated Maestro version string
25
26
  * @throws Error if version is not supported
26
27
  */
@@ -44,8 +44,9 @@ class VersionService {
44
44
  * Resolve and validate Maestro version against API compatibility data
45
45
  * @param requestedVersion - Version requested by user (or undefined for default)
46
46
  * @param compatibilityData - API compatibility data
47
- * @param debug - Enable debug logging
48
- * @param logger - Optional logger function
47
+ * @param options - Configuration options
48
+ * @param options.debug - Enable debug logging
49
+ * @param options.logger - Optional logger function
49
50
  * @returns Validated Maestro version string
50
51
  * @throws Error if version is not supported
51
52
  */