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