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