@devicecloud.dev/dcd 4.2.3 → 4.2.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.
- package/dist/commands/cloud.d.ts +4 -2
- package/dist/commands/cloud.js +29 -5
- package/dist/commands/status.d.ts +2 -2
- package/dist/config/flags/device.flags.d.ts +2 -0
- package/dist/config/flags/device.flags.js +9 -1
- package/dist/config/flags/output.flags.js +5 -5
- package/dist/constants.d.ts +2 -0
- package/dist/services/execution-plan.service.js +2 -1
- package/dist/services/results-polling.service.d.ts +16 -1
- package/dist/services/results-polling.service.js +21 -8
- package/dist/services/test-submission.service.d.ts +2 -0
- package/dist/services/test-submission.service.js +3 -1
- package/dist/utils/styling.d.ts +2 -0
- package/dist/utils/styling.js +5 -0
- package/oclif.manifest.json +19 -7
- package/package.json +1 -1
package/dist/commands/cloud.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { Command } from '@oclif/core';
|
|
|
6
6
|
* - Flow file analysis and dependency resolution
|
|
7
7
|
* - Device compatibility validation
|
|
8
8
|
* - Test submission with parallel execution
|
|
9
|
-
* - Real-time result polling with
|
|
9
|
+
* - Real-time result polling with 10-second intervals
|
|
10
10
|
* - Artifact download (reports, videos, logs)
|
|
11
11
|
*
|
|
12
12
|
* Replaces `maestro cloud` with DeviceCloud-specific functionality.
|
|
@@ -55,6 +55,8 @@ export default class Cloud extends Command {
|
|
|
55
55
|
'ios-version': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
56
56
|
orientation: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
57
57
|
'show-crosshairs': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
58
|
+
'maestro-chrome-onboarding': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
59
|
+
'android-no-snapshot': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
58
60
|
'app-binary-id': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
59
61
|
'app-file': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
60
62
|
'ignore-sha-check': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
@@ -67,7 +69,7 @@ export default class Cloud extends Command {
|
|
|
67
69
|
private moropoService;
|
|
68
70
|
/** Service for downloading test reports and artifacts */
|
|
69
71
|
private reportDownloadService;
|
|
70
|
-
/** Service for polling test results with
|
|
72
|
+
/** Service for polling test results with 10-second intervals */
|
|
71
73
|
private resultsPollingService;
|
|
72
74
|
/** Service for submitting tests to the API */
|
|
73
75
|
private testSubmissionService;
|
package/dist/commands/cloud.js
CHANGED
|
@@ -31,7 +31,7 @@ process.on('warning', (warning) => {
|
|
|
31
31
|
* - Flow file analysis and dependency resolution
|
|
32
32
|
* - Device compatibility validation
|
|
33
33
|
* - Test submission with parallel execution
|
|
34
|
-
* - Real-time result polling with
|
|
34
|
+
* - Real-time result polling with 10-second intervals
|
|
35
35
|
* - Artifact download (reports, videos, logs)
|
|
36
36
|
*
|
|
37
37
|
* Replaces `maestro cloud` with DeviceCloud-specific functionality.
|
|
@@ -59,7 +59,7 @@ class Cloud extends core_1.Command {
|
|
|
59
59
|
moropoService = new moropo_service_1.MoropoService();
|
|
60
60
|
/** Service for downloading test reports and artifacts */
|
|
61
61
|
reportDownloadService = new report_download_service_1.ReportDownloadService();
|
|
62
|
-
/** Service for polling test results with
|
|
62
|
+
/** Service for polling test results with 10-second intervals */
|
|
63
63
|
resultsPollingService = new results_polling_service_1.ResultsPollingService();
|
|
64
64
|
/** Service for submitting tests to the API */
|
|
65
65
|
testSubmissionService = new test_submission_service_1.TestSubmissionService();
|
|
@@ -95,7 +95,7 @@ class Cloud extends core_1.Command {
|
|
|
95
95
|
let jsonFile = false;
|
|
96
96
|
try {
|
|
97
97
|
const { args, flags, raw } = await this.parse(Cloud);
|
|
98
|
-
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;
|
|
98
|
+
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, 'android-no-snapshot': androidNoSnapshot, } = flags;
|
|
99
99
|
// Store debug flag for use in catch block
|
|
100
100
|
debugFlag = debug === true;
|
|
101
101
|
jsonFile = flags['json-file'] === true;
|
|
@@ -202,6 +202,10 @@ class Cloud extends core_1.Command {
|
|
|
202
202
|
this.deviceValidationService.validateiOSDevice(iOSVersion, iOSDevice, compatibilityData, { debug, logger: this.log.bind(this) });
|
|
203
203
|
// Validate Android device configuration
|
|
204
204
|
this.deviceValidationService.validateAndroidDevice(androidApiLevel, androidDevice, googlePlay, compatibilityData, { debug, logger: this.log.bind(this) });
|
|
205
|
+
// Warn if maestro-chrome-onboarding flag is used without Android devices
|
|
206
|
+
if (flags['maestro-chrome-onboarding'] && !androidApiLevel && !androidDevice) {
|
|
207
|
+
this.warn('The --maestro-chrome-onboarding flag only applies to Android tests and will be ignored for iOS tests.');
|
|
208
|
+
}
|
|
205
209
|
flowFile = path.resolve(flowFile);
|
|
206
210
|
if (!flowFile?.endsWith('.yaml') &&
|
|
207
211
|
!flowFile?.endsWith('.yml') &&
|
|
@@ -238,7 +242,7 @@ class Cloud extends core_1.Command {
|
|
|
238
242
|
}
|
|
239
243
|
throw error;
|
|
240
244
|
}
|
|
241
|
-
const { allExcludeTags, allIncludeTags, flowOverrides, flowsToRun: testFileNames, referencedFiles, sequence, } = executionPlan;
|
|
245
|
+
const { allExcludeTags, allIncludeTags, flowMetadata, flowOverrides, flowsToRun: testFileNames, referencedFiles, sequence, } = executionPlan;
|
|
242
246
|
if (debug) {
|
|
243
247
|
this.log(`[DEBUG] All include tags: ${allIncludeTags?.join(', ') || 'none'}`);
|
|
244
248
|
this.log(`[DEBUG] All exclude tags: ${allExcludeTags?.join(', ') || 'none'}`);
|
|
@@ -259,6 +263,21 @@ class Cloud extends core_1.Command {
|
|
|
259
263
|
if (debug) {
|
|
260
264
|
this.log(`[DEBUG] Common root directory: ${commonRoot}`);
|
|
261
265
|
}
|
|
266
|
+
// Build testMetadataMap from flowMetadata (keyed by normalized test file name)
|
|
267
|
+
// This map provides flowName and tags for each test for JSON output
|
|
268
|
+
const testMetadataMap = {};
|
|
269
|
+
for (const [absolutePath, metadata] of Object.entries(flowMetadata)) {
|
|
270
|
+
// Normalize the path to match the format used in results (e.g., "./flows/test.yaml")
|
|
271
|
+
const normalizedPath = absolutePath.replaceAll(commonRoot, '.').split(path.sep).join('/');
|
|
272
|
+
const metadataRecord = metadata;
|
|
273
|
+
const flowName = metadataRecord?.name || path.parse(absolutePath).name;
|
|
274
|
+
const rawTags = metadataRecord?.tags;
|
|
275
|
+
const tags = Array.isArray(rawTags) ? rawTags.map(String) : (rawTags ? [String(rawTags)] : []);
|
|
276
|
+
testMetadataMap[normalizedPath] = { flowName, tags };
|
|
277
|
+
}
|
|
278
|
+
if (debug) {
|
|
279
|
+
this.log(`[DEBUG] Built testMetadataMap for ${Object.keys(testMetadataMap).length} flows`);
|
|
280
|
+
}
|
|
262
281
|
const { continueOnFailure = true, flows: sequentialFlows = [] } = sequence ?? {};
|
|
263
282
|
if (debug && sequentialFlows.length > 0) {
|
|
264
283
|
this.log(`[DEBUG] Sequential flows: ${sequentialFlows.join(', ')}`);
|
|
@@ -359,6 +378,7 @@ class Cloud extends core_1.Command {
|
|
|
359
378
|
const testFormData = await this.testSubmissionService.buildTestFormData({
|
|
360
379
|
androidApiLevel,
|
|
361
380
|
androidDevice,
|
|
381
|
+
androidNoSnapshot,
|
|
362
382
|
appBinaryId: finalBinaryId,
|
|
363
383
|
cliVersion: this.config.version,
|
|
364
384
|
commonRoot,
|
|
@@ -383,6 +403,7 @@ class Cloud extends core_1.Command {
|
|
|
383
403
|
retry,
|
|
384
404
|
runnerType,
|
|
385
405
|
showCrosshairs: flags['show-crosshairs'],
|
|
406
|
+
maestroChromeOnboarding: flags['maestro-chrome-onboarding'],
|
|
386
407
|
});
|
|
387
408
|
if (debug) {
|
|
388
409
|
this.log(`[DEBUG] Submitting flow upload request to ${apiUrl}/uploads/flow`);
|
|
@@ -415,8 +436,11 @@ class Cloud extends core_1.Command {
|
|
|
415
436
|
consoleUrl: url,
|
|
416
437
|
status: 'PENDING',
|
|
417
438
|
tests: results.map((r) => ({
|
|
439
|
+
fileName: r.test_file_name,
|
|
440
|
+
flowName: testMetadataMap[r.test_file_name]?.flowName || path.parse(r.test_file_name).name,
|
|
418
441
|
name: r.test_file_name,
|
|
419
442
|
status: r.status,
|
|
443
|
+
tags: testMetadataMap[r.test_file_name]?.tags || [],
|
|
420
444
|
})),
|
|
421
445
|
uploadId: results[0].test_upload_id,
|
|
422
446
|
};
|
|
@@ -441,7 +465,7 @@ class Cloud extends core_1.Command {
|
|
|
441
465
|
logger: this.log.bind(this),
|
|
442
466
|
quiet,
|
|
443
467
|
uploadId: results[0].test_upload_id,
|
|
444
|
-
})
|
|
468
|
+
}, testMetadataMap)
|
|
445
469
|
.catch(async (error) => {
|
|
446
470
|
if (error instanceof results_polling_service_1.RunFailedError) {
|
|
447
471
|
// Handle failed test run
|
|
@@ -12,13 +12,13 @@ type StatusResponse = {
|
|
|
12
12
|
createdAt?: string;
|
|
13
13
|
error?: string;
|
|
14
14
|
name?: string;
|
|
15
|
-
status: 'CANCELLED' | 'FAILED' | 'PASSED' | 'PENDING' | 'RUNNING';
|
|
15
|
+
status: 'CANCELLED' | 'FAILED' | 'PASSED' | 'PENDING' | 'QUEUED' | 'RUNNING';
|
|
16
16
|
tests: {
|
|
17
17
|
createdAt?: string;
|
|
18
18
|
durationSeconds?: number;
|
|
19
19
|
failReason?: string;
|
|
20
20
|
name: string;
|
|
21
|
-
status: 'CANCELLED' | 'FAILED' | 'PASSED' | 'PENDING' | 'RUNNING';
|
|
21
|
+
status: 'CANCELLED' | 'FAILED' | 'PASSED' | 'PENDING' | 'QUEUED' | 'RUNNING';
|
|
22
22
|
}[];
|
|
23
23
|
uploadId?: string;
|
|
24
24
|
};
|
|
@@ -10,4 +10,6 @@ export declare const deviceFlags: {
|
|
|
10
10
|
'ios-version': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
11
11
|
orientation: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
12
12
|
'show-crosshairs': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
'maestro-chrome-onboarding': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
'android-no-snapshot': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
13
15
|
};
|
|
@@ -32,11 +32,19 @@ exports.deviceFlags = {
|
|
|
32
32
|
options: Object.values(device_types_1.EiOSVersions),
|
|
33
33
|
}),
|
|
34
34
|
orientation: core_1.Flags.string({
|
|
35
|
-
description: '[Android only] The orientation of the device to run your flow against
|
|
35
|
+
description: '[Android only] The orientation of the device to run your flow against (0 = portrait, 90 = landscape)',
|
|
36
36
|
options: ['0', '90'],
|
|
37
37
|
}),
|
|
38
38
|
'show-crosshairs': core_1.Flags.boolean({
|
|
39
39
|
default: false,
|
|
40
40
|
description: '[Android only] Display crosshairs for screen interactions during test execution',
|
|
41
41
|
}),
|
|
42
|
+
'maestro-chrome-onboarding': core_1.Flags.boolean({
|
|
43
|
+
default: false,
|
|
44
|
+
description: '[Android only] Force Maestro-based Chrome onboarding - note: this will slow your tests but can fix browser related crashes. See https://docs.devicecloud.dev/reference/chrome-onboarding for more information.',
|
|
45
|
+
}),
|
|
46
|
+
'android-no-snapshot': core_1.Flags.boolean({
|
|
47
|
+
default: false,
|
|
48
|
+
description: '[Android only] Force cold boot instead of using snapshot boot. This is automatically enabled for API 35+ but can be used to force cold boot on older API levels.',
|
|
49
|
+
}),
|
|
42
50
|
};
|
|
@@ -12,15 +12,15 @@ exports.outputFlags = {
|
|
|
12
12
|
}),
|
|
13
13
|
'junit-path': core_1.Flags.string({
|
|
14
14
|
dependsOn: ['report'],
|
|
15
|
-
description: 'Custom file path for downloaded JUnit report (default: ./report.xml)',
|
|
15
|
+
description: 'Custom file path for downloaded JUnit report (requires --report junit, default: ./report.xml)',
|
|
16
16
|
}),
|
|
17
17
|
'allure-path': core_1.Flags.string({
|
|
18
18
|
dependsOn: ['report'],
|
|
19
|
-
description: 'Custom file path for downloaded Allure report (default: ./report.html)',
|
|
19
|
+
description: 'Custom file path for downloaded Allure report (requires --report allure, default: ./report.html)',
|
|
20
20
|
}),
|
|
21
21
|
'html-path': core_1.Flags.string({
|
|
22
22
|
dependsOn: ['report'],
|
|
23
|
-
description: 'Custom file path for downloaded HTML report (default: ./report.html)',
|
|
23
|
+
description: 'Custom file path for downloaded HTML report (requires --report html, default: ./report.html)',
|
|
24
24
|
}),
|
|
25
25
|
async: core_1.Flags.boolean({
|
|
26
26
|
description: 'Immediately return (exit code 0) from the command without waiting for the results of the run (useful for saving CI minutes)',
|
|
@@ -41,7 +41,7 @@ exports.outputFlags = {
|
|
|
41
41
|
description: 'Output results in JSON format - note: will always provide exit code 0',
|
|
42
42
|
}),
|
|
43
43
|
'json-file': core_1.Flags.boolean({
|
|
44
|
-
description: 'Write JSON output to a file. File be called <upload_id>_dcd.json unless you supply the --json-file-name flag - note: will always exit with code 0',
|
|
44
|
+
description: 'Write JSON output to a file. File will be called <upload_id>_dcd.json unless you supply the --json-file-name flag - note: will always exit with code 0',
|
|
45
45
|
required: false,
|
|
46
46
|
}),
|
|
47
47
|
'json-file-name': core_1.Flags.string({
|
|
@@ -51,7 +51,7 @@ exports.outputFlags = {
|
|
|
51
51
|
quiet: core_1.Flags.boolean({
|
|
52
52
|
char: 'q',
|
|
53
53
|
default: false,
|
|
54
|
-
description:
|
|
54
|
+
description: "Quieter console output that won't provide progress updates",
|
|
55
55
|
}),
|
|
56
56
|
report: core_1.Flags.string({
|
|
57
57
|
aliases: ['format'],
|
package/dist/constants.d.ts
CHANGED
|
@@ -42,6 +42,8 @@ export declare const flags: {
|
|
|
42
42
|
'ios-version': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
43
43
|
orientation: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
44
44
|
'show-crosshairs': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
45
|
+
'maestro-chrome-onboarding': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
46
|
+
'android-no-snapshot': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
45
47
|
'app-binary-id': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
46
48
|
'app-file': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
47
49
|
'ignore-sha-check': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
@@ -211,7 +211,8 @@ async function plan(options) {
|
|
|
211
211
|
const flowOverrides = {};
|
|
212
212
|
const allFlows = unfilteredFlowFiles.filter((filePath) => {
|
|
213
213
|
const config = configPerFlowFile[filePath];
|
|
214
|
-
const
|
|
214
|
+
const rawTags = config?.tags;
|
|
215
|
+
const tags = Array.isArray(rawTags) ? rawTags : (rawTags ? [rawTags] : []);
|
|
215
216
|
if (config) {
|
|
216
217
|
flowMetadata[filePath] = config;
|
|
217
218
|
flowOverrides[filePath] = extractDeviceCloudOverrides(config);
|
|
@@ -17,14 +17,28 @@ export interface PollingOptions {
|
|
|
17
17
|
quiet?: boolean;
|
|
18
18
|
uploadId: string;
|
|
19
19
|
}
|
|
20
|
+
/** Metadata for a test flow extracted from YAML config */
|
|
21
|
+
export interface TestMetadata {
|
|
22
|
+
/** Flow name from YAML config 'name' field or filename without extension */
|
|
23
|
+
flowName: string;
|
|
24
|
+
/** Tags from YAML config 'tags' field */
|
|
25
|
+
tags: string[];
|
|
26
|
+
}
|
|
20
27
|
export interface PollingResult {
|
|
21
28
|
consoleUrl: string;
|
|
22
29
|
status: 'FAILED' | 'PASSED';
|
|
23
30
|
tests: Array<{
|
|
24
31
|
durationSeconds: null | number;
|
|
25
32
|
failReason?: string;
|
|
33
|
+
/** File path of the test (same as name, for clarity) */
|
|
34
|
+
fileName: string;
|
|
35
|
+
/** Flow name from YAML config or filename without extension */
|
|
36
|
+
flowName: string;
|
|
37
|
+
/** Test file name (unchanged for backwards compatibility) */
|
|
26
38
|
name: string;
|
|
27
39
|
status: string;
|
|
40
|
+
/** Tags from YAML config (empty array if none) */
|
|
41
|
+
tags: string[];
|
|
28
42
|
}>;
|
|
29
43
|
uploadId: string;
|
|
30
44
|
}
|
|
@@ -38,9 +52,10 @@ export declare class ResultsPollingService {
|
|
|
38
52
|
* Poll for test results until all tests complete
|
|
39
53
|
* @param results Initial test results from submission
|
|
40
54
|
* @param options Polling configuration
|
|
55
|
+
* @param testMetadata Optional metadata map for each test (flowName, tags)
|
|
41
56
|
* @returns Promise that resolves with final test results or rejects if tests fail
|
|
42
57
|
*/
|
|
43
|
-
pollUntilComplete(results: TestResult[], options: PollingOptions): Promise<PollingResult>;
|
|
58
|
+
pollUntilComplete(results: TestResult[], options: PollingOptions, testMetadata?: Record<string, TestMetadata>): Promise<PollingResult>;
|
|
44
59
|
private buildPollingResult;
|
|
45
60
|
private calculateStatusSummary;
|
|
46
61
|
private displayFinalResults;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ResultsPollingService = exports.RunFailedError = void 0;
|
|
4
4
|
const core_1 = require("@oclif/core");
|
|
5
5
|
const cli_ux_1 = require("@oclif/core/lib/cli-ux");
|
|
6
|
+
const path = require("node:path");
|
|
6
7
|
const api_gateway_1 = require("../gateways/api-gateway");
|
|
7
8
|
const methods_1 = require("../methods");
|
|
8
9
|
const connectivity_1 = require("../utils/connectivity");
|
|
@@ -24,14 +25,15 @@ exports.RunFailedError = RunFailedError;
|
|
|
24
25
|
*/
|
|
25
26
|
class ResultsPollingService {
|
|
26
27
|
MAX_SEQUENTIAL_FAILURES = 10;
|
|
27
|
-
POLL_INTERVAL_MS =
|
|
28
|
+
POLL_INTERVAL_MS = 10_000;
|
|
28
29
|
/**
|
|
29
30
|
* Poll for test results until all tests complete
|
|
30
31
|
* @param results Initial test results from submission
|
|
31
32
|
* @param options Polling configuration
|
|
33
|
+
* @param testMetadata Optional metadata map for each test (flowName, tags)
|
|
32
34
|
* @returns Promise that resolves with final test results or rejects if tests fail
|
|
33
35
|
*/
|
|
34
|
-
async pollUntilComplete(results, options) {
|
|
36
|
+
async pollUntilComplete(results, options, testMetadata) {
|
|
35
37
|
const { apiUrl, apiKey, uploadId, consoleUrl, quiet = false, json = false, debug = false, logger } = options;
|
|
36
38
|
this.initializePollingDisplay(json, logger);
|
|
37
39
|
let sequentialPollFailures = 0;
|
|
@@ -46,13 +48,14 @@ class ResultsPollingService {
|
|
|
46
48
|
const updatedResults = await this.fetchAndLogResults(apiUrl, apiKey, uploadId, debug, logger);
|
|
47
49
|
const { summary } = this.calculateStatusSummary(updatedResults);
|
|
48
50
|
previousSummary = this.updateDisplayStatus(updatedResults, quiet, json, summary, previousSummary);
|
|
49
|
-
const allComplete = updatedResults.every((result) => !['PENDING', 'RUNNING'].includes(result.status));
|
|
51
|
+
const allComplete = updatedResults.every((result) => !['PENDING', 'QUEUED', 'RUNNING'].includes(result.status));
|
|
50
52
|
if (allComplete) {
|
|
51
53
|
return await this.handleCompletedTests(updatedResults, {
|
|
52
54
|
consoleUrl,
|
|
53
55
|
debug,
|
|
54
56
|
json,
|
|
55
57
|
logger,
|
|
58
|
+
testMetadata,
|
|
56
59
|
uploadId,
|
|
57
60
|
});
|
|
58
61
|
}
|
|
@@ -74,7 +77,7 @@ class ResultsPollingService {
|
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
79
|
}
|
|
77
|
-
buildPollingResult(results, uploadId, consoleUrl) {
|
|
80
|
+
buildPollingResult(results, uploadId, consoleUrl, testMetadata) {
|
|
78
81
|
const resultsWithoutEarlierTries = this.filterLatestResults(results);
|
|
79
82
|
return {
|
|
80
83
|
consoleUrl,
|
|
@@ -84,8 +87,11 @@ class ResultsPollingService {
|
|
|
84
87
|
tests: resultsWithoutEarlierTries.map((r) => ({
|
|
85
88
|
durationSeconds: r.duration_seconds,
|
|
86
89
|
failReason: r.status === 'FAILED' ? r.fail_reason || 'No reason provided' : undefined,
|
|
90
|
+
fileName: r.test_file_name,
|
|
91
|
+
flowName: testMetadata?.[r.test_file_name]?.flowName || path.parse(r.test_file_name).name,
|
|
87
92
|
name: r.test_file_name,
|
|
88
93
|
status: r.status,
|
|
94
|
+
tags: testMetadata?.[r.test_file_name]?.tags || [],
|
|
89
95
|
})),
|
|
90
96
|
uploadId,
|
|
91
97
|
};
|
|
@@ -98,6 +104,7 @@ class ResultsPollingService {
|
|
|
98
104
|
const passed = statusCounts.PASSED || 0;
|
|
99
105
|
const failed = statusCounts.FAILED || 0;
|
|
100
106
|
const pending = statusCounts.PENDING || 0;
|
|
107
|
+
const queued = statusCounts.QUEUED || 0;
|
|
101
108
|
const running = statusCounts.RUNNING || 0;
|
|
102
109
|
const total = results.length;
|
|
103
110
|
const completed = passed + failed;
|
|
@@ -106,10 +113,11 @@ class ResultsPollingService {
|
|
|
106
113
|
failed,
|
|
107
114
|
passed,
|
|
108
115
|
pending,
|
|
116
|
+
queued,
|
|
109
117
|
running,
|
|
110
118
|
total,
|
|
111
119
|
});
|
|
112
|
-
return { completed, failed, passed, pending, running, summary, total };
|
|
120
|
+
return { completed, failed, passed, pending, queued, running, summary, total };
|
|
113
121
|
}
|
|
114
122
|
displayFinalResults(results, consoleUrl, json, logger) {
|
|
115
123
|
if (json) {
|
|
@@ -144,6 +152,9 @@ class ResultsPollingService {
|
|
|
144
152
|
case 'PENDING': {
|
|
145
153
|
return styling_1.colors.warning(row.status);
|
|
146
154
|
}
|
|
155
|
+
case 'QUEUED': {
|
|
156
|
+
return styling_1.colors.dim(row.status);
|
|
157
|
+
}
|
|
147
158
|
default: {
|
|
148
159
|
return styling_1.colors.dim(row.status);
|
|
149
160
|
}
|
|
@@ -214,12 +225,12 @@ class ResultsPollingService {
|
|
|
214
225
|
* @returns Promise resolving to final polling result
|
|
215
226
|
*/
|
|
216
227
|
async handleCompletedTests(updatedResults, options) {
|
|
217
|
-
const { uploadId, consoleUrl, json, debug, logger } = options;
|
|
228
|
+
const { uploadId, consoleUrl, json, debug, logger, testMetadata } = options;
|
|
218
229
|
if (debug && logger) {
|
|
219
230
|
logger(`[DEBUG] All tests completed, stopping poll`);
|
|
220
231
|
}
|
|
221
232
|
this.displayFinalResults(updatedResults, consoleUrl, json, logger);
|
|
222
|
-
const output = this.buildPollingResult(updatedResults, uploadId, consoleUrl);
|
|
233
|
+
const output = this.buildPollingResult(updatedResults, uploadId, consoleUrl, testMetadata);
|
|
223
234
|
if (output.status === 'FAILED') {
|
|
224
235
|
if (debug && logger) {
|
|
225
236
|
logger(`[DEBUG] Some tests failed, returning failed status`);
|
|
@@ -309,7 +320,9 @@ class ResultsPollingService {
|
|
|
309
320
|
? styling_1.colors.error(status.padEnd(10, ' '))
|
|
310
321
|
: status.toUpperCase() === 'RUNNING'
|
|
311
322
|
? styling_1.colors.info(status.padEnd(10, ' '))
|
|
312
|
-
:
|
|
323
|
+
: status.toUpperCase() === 'QUEUED'
|
|
324
|
+
? styling_1.colors.dim(status.padEnd(10, ' '))
|
|
325
|
+
: styling_1.colors.warning(status.padEnd(10, ' '));
|
|
313
326
|
const retryText = isRetry ? styling_1.colors.dim(' (retry)') : '';
|
|
314
327
|
core_1.ux.action.status += `\n${statusFormatted} ${test}${retryText}`;
|
|
315
328
|
}
|
|
@@ -2,6 +2,7 @@ import { IExecutionPlan } from './execution-plan.service';
|
|
|
2
2
|
export interface TestSubmissionConfig {
|
|
3
3
|
androidApiLevel?: string;
|
|
4
4
|
androidDevice?: string;
|
|
5
|
+
androidNoSnapshot?: boolean;
|
|
5
6
|
appBinaryId: string;
|
|
6
7
|
cliVersion: string;
|
|
7
8
|
commonRoot: string;
|
|
@@ -15,6 +16,7 @@ export interface TestSubmissionConfig {
|
|
|
15
16
|
iOSDevice?: string;
|
|
16
17
|
iOSVersion?: string;
|
|
17
18
|
logger?: (message: string) => void;
|
|
19
|
+
maestroChromeOnboarding?: boolean;
|
|
18
20
|
maestroVersion: string;
|
|
19
21
|
metadata?: string[];
|
|
20
22
|
mitmHost?: string;
|
|
@@ -17,7 +17,7 @@ class TestSubmissionService {
|
|
|
17
17
|
* @returns FormData ready to be submitted to the API
|
|
18
18
|
*/
|
|
19
19
|
async buildTestFormData(config) {
|
|
20
|
-
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, raw, debug = false, logger, } = config;
|
|
20
|
+
const { appBinaryId, flowFile, executionPlan, commonRoot, cliVersion, env = [], metadata = [], googlePlay = false, androidApiLevel, androidDevice, androidNoSnapshot, iOSVersion, iOSDevice, name, runnerType, maestroVersion, deviceLocale, orientation, mitmHost, mitmPath, retry, continueOnFailure = true, report, showCrosshairs, maestroChromeOnboarding, raw, debug = false, logger, } = config;
|
|
21
21
|
const { allExcludeTags, allIncludeTags, flowMetadata, flowOverrides, flowsToRun: testFileNames, referencedFiles, sequence, workspaceConfig, } = executionPlan;
|
|
22
22
|
const { flows: sequentialFlows = [] } = sequence ?? {};
|
|
23
23
|
const testFormData = new FormData();
|
|
@@ -69,6 +69,7 @@ class TestSubmissionService {
|
|
|
69
69
|
const configPayload = {
|
|
70
70
|
allExcludeTags,
|
|
71
71
|
allIncludeTags,
|
|
72
|
+
androidNoSnapshot,
|
|
72
73
|
autoRetriesRemaining: retry,
|
|
73
74
|
continueOnFailure,
|
|
74
75
|
deviceLocale,
|
|
@@ -79,6 +80,7 @@ class TestSubmissionService {
|
|
|
79
80
|
raw: JSON.stringify(raw),
|
|
80
81
|
report,
|
|
81
82
|
showCrosshairs,
|
|
83
|
+
maestroChromeOnboarding,
|
|
82
84
|
version: cliVersion,
|
|
83
85
|
};
|
|
84
86
|
testFormData.set('config', JSON.stringify(configPayload));
|
package/dist/utils/styling.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare const symbols: {
|
|
|
11
11
|
readonly error: string;
|
|
12
12
|
readonly info: string;
|
|
13
13
|
readonly pending: string;
|
|
14
|
+
readonly queued: string;
|
|
14
15
|
readonly running: string;
|
|
15
16
|
readonly success: string;
|
|
16
17
|
readonly unknown: string;
|
|
@@ -86,6 +87,7 @@ export declare function formatTestSummary(summary: {
|
|
|
86
87
|
failed: number;
|
|
87
88
|
passed: number;
|
|
88
89
|
pending: number;
|
|
90
|
+
queued: number;
|
|
89
91
|
running: number;
|
|
90
92
|
total: number;
|
|
91
93
|
}): string;
|
package/dist/utils/styling.js
CHANGED
|
@@ -23,6 +23,7 @@ exports.symbols = {
|
|
|
23
23
|
error: chalk.red('✗'),
|
|
24
24
|
info: chalk.blue('ℹ'),
|
|
25
25
|
pending: chalk.yellow('⏸'),
|
|
26
|
+
queued: chalk.gray('⏳'),
|
|
26
27
|
running: chalk.blue('▶'),
|
|
27
28
|
success: chalk.green('✓'),
|
|
28
29
|
unknown: chalk.gray('?'),
|
|
@@ -69,6 +70,9 @@ function formatStatus(status) {
|
|
|
69
70
|
case 'PENDING': {
|
|
70
71
|
return `${exports.symbols.pending} ${exports.colors.warning(status)}`;
|
|
71
72
|
}
|
|
73
|
+
case 'QUEUED': {
|
|
74
|
+
return `${exports.symbols.queued} ${exports.colors.dim(status)}`;
|
|
75
|
+
}
|
|
72
76
|
case 'CANCELLED': {
|
|
73
77
|
return `${exports.symbols.cancelled} ${exports.colors.dim(status)}`;
|
|
74
78
|
}
|
|
@@ -132,6 +136,7 @@ function formatTestSummary(summary) {
|
|
|
132
136
|
exports.colors.error(`✗ ${summary.failed}`),
|
|
133
137
|
exports.colors.info(`▶ ${summary.running}`),
|
|
134
138
|
exports.colors.warning(`⏸ ${summary.pending}`),
|
|
139
|
+
exports.colors.dim(`⏳ ${summary.queued}`),
|
|
135
140
|
];
|
|
136
141
|
return parts.join(' │ ');
|
|
137
142
|
}
|
package/oclif.manifest.json
CHANGED
|
@@ -153,7 +153,7 @@
|
|
|
153
153
|
"type": "option"
|
|
154
154
|
},
|
|
155
155
|
"orientation": {
|
|
156
|
-
"description": "[Android only] The orientation of the device to run your flow against
|
|
156
|
+
"description": "[Android only] The orientation of the device to run your flow against (0 = portrait, 90 = landscape)",
|
|
157
157
|
"name": "orientation",
|
|
158
158
|
"hasDynamicHelp": false,
|
|
159
159
|
"multiple": false,
|
|
@@ -169,6 +169,18 @@
|
|
|
169
169
|
"allowNo": false,
|
|
170
170
|
"type": "boolean"
|
|
171
171
|
},
|
|
172
|
+
"maestro-chrome-onboarding": {
|
|
173
|
+
"description": "[Android only] Force Maestro-based Chrome onboarding - note: this will slow your tests but can fix browser related crashes. See https://docs.devicecloud.dev/reference/chrome-onboarding for more information.",
|
|
174
|
+
"name": "maestro-chrome-onboarding",
|
|
175
|
+
"allowNo": false,
|
|
176
|
+
"type": "boolean"
|
|
177
|
+
},
|
|
178
|
+
"android-no-snapshot": {
|
|
179
|
+
"description": "[Android only] Force cold boot instead of using snapshot boot. This is automatically enabled for API 35+ but can be used to force cold boot on older API levels.",
|
|
180
|
+
"name": "android-no-snapshot",
|
|
181
|
+
"allowNo": false,
|
|
182
|
+
"type": "boolean"
|
|
183
|
+
},
|
|
172
184
|
"env": {
|
|
173
185
|
"char": "e",
|
|
174
186
|
"description": "One or more environment variables to inject into your flows",
|
|
@@ -309,7 +321,7 @@
|
|
|
309
321
|
"dependsOn": [
|
|
310
322
|
"report"
|
|
311
323
|
],
|
|
312
|
-
"description": "Custom file path for downloaded JUnit report (default: ./report.xml)",
|
|
324
|
+
"description": "Custom file path for downloaded JUnit report (requires --report junit, default: ./report.xml)",
|
|
313
325
|
"name": "junit-path",
|
|
314
326
|
"hasDynamicHelp": false,
|
|
315
327
|
"multiple": false,
|
|
@@ -319,7 +331,7 @@
|
|
|
319
331
|
"dependsOn": [
|
|
320
332
|
"report"
|
|
321
333
|
],
|
|
322
|
-
"description": "Custom file path for downloaded Allure report (default: ./report.html)",
|
|
334
|
+
"description": "Custom file path for downloaded Allure report (requires --report allure, default: ./report.html)",
|
|
323
335
|
"name": "allure-path",
|
|
324
336
|
"hasDynamicHelp": false,
|
|
325
337
|
"multiple": false,
|
|
@@ -329,7 +341,7 @@
|
|
|
329
341
|
"dependsOn": [
|
|
330
342
|
"report"
|
|
331
343
|
],
|
|
332
|
-
"description": "Custom file path for downloaded HTML report (default: ./report.html)",
|
|
344
|
+
"description": "Custom file path for downloaded HTML report (requires --report html, default: ./report.html)",
|
|
333
345
|
"name": "html-path",
|
|
334
346
|
"hasDynamicHelp": false,
|
|
335
347
|
"multiple": false,
|
|
@@ -365,7 +377,7 @@
|
|
|
365
377
|
"type": "boolean"
|
|
366
378
|
},
|
|
367
379
|
"json-file": {
|
|
368
|
-
"description": "Write JSON output to a file. File be called <upload_id>_dcd.json unless you supply the --json-file-name flag - note: will always exit with code 0",
|
|
380
|
+
"description": "Write JSON output to a file. File will be called <upload_id>_dcd.json unless you supply the --json-file-name flag - note: will always exit with code 0",
|
|
369
381
|
"name": "json-file",
|
|
370
382
|
"required": false,
|
|
371
383
|
"allowNo": false,
|
|
@@ -383,7 +395,7 @@
|
|
|
383
395
|
},
|
|
384
396
|
"quiet": {
|
|
385
397
|
"char": "q",
|
|
386
|
-
"description": "Quieter console output that
|
|
398
|
+
"description": "Quieter console output that won't provide progress updates",
|
|
387
399
|
"name": "quiet",
|
|
388
400
|
"allowNo": false,
|
|
389
401
|
"type": "boolean"
|
|
@@ -661,5 +673,5 @@
|
|
|
661
673
|
]
|
|
662
674
|
}
|
|
663
675
|
},
|
|
664
|
-
"version": "4.2.
|
|
676
|
+
"version": "4.2.5"
|
|
665
677
|
}
|