@devicecloud.dev/dcd 4.2.4 → 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 +3 -2
- package/dist/commands/cloud.js +24 -5
- package/dist/config/flags/device.flags.d.ts +1 -0
- package/dist/config/flags/device.flags.js +4 -0
- package/dist/constants.d.ts +1 -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 +11 -5
- package/dist/services/test-submission.service.d.ts +1 -0
- package/dist/services/test-submission.service.js +2 -1
- package/oclif.manifest.json +7 -1
- 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.
|
|
@@ -56,6 +56,7 @@ export default class Cloud extends Command {
|
|
|
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
58
|
'maestro-chrome-onboarding': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
59
|
+
'android-no-snapshot': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
59
60
|
'app-binary-id': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
60
61
|
'app-file': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
61
62
|
'ignore-sha-check': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
@@ -68,7 +69,7 @@ export default class Cloud extends Command {
|
|
|
68
69
|
private moropoService;
|
|
69
70
|
/** Service for downloading test reports and artifacts */
|
|
70
71
|
private reportDownloadService;
|
|
71
|
-
/** Service for polling test results with
|
|
72
|
+
/** Service for polling test results with 10-second intervals */
|
|
72
73
|
private resultsPollingService;
|
|
73
74
|
/** Service for submitting tests to the API */
|
|
74
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;
|
|
@@ -242,7 +242,7 @@ class Cloud extends core_1.Command {
|
|
|
242
242
|
}
|
|
243
243
|
throw error;
|
|
244
244
|
}
|
|
245
|
-
const { allExcludeTags, allIncludeTags, flowOverrides, flowsToRun: testFileNames, referencedFiles, sequence, } = executionPlan;
|
|
245
|
+
const { allExcludeTags, allIncludeTags, flowMetadata, flowOverrides, flowsToRun: testFileNames, referencedFiles, sequence, } = executionPlan;
|
|
246
246
|
if (debug) {
|
|
247
247
|
this.log(`[DEBUG] All include tags: ${allIncludeTags?.join(', ') || 'none'}`);
|
|
248
248
|
this.log(`[DEBUG] All exclude tags: ${allExcludeTags?.join(', ') || 'none'}`);
|
|
@@ -263,6 +263,21 @@ class Cloud extends core_1.Command {
|
|
|
263
263
|
if (debug) {
|
|
264
264
|
this.log(`[DEBUG] Common root directory: ${commonRoot}`);
|
|
265
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
|
+
}
|
|
266
281
|
const { continueOnFailure = true, flows: sequentialFlows = [] } = sequence ?? {};
|
|
267
282
|
if (debug && sequentialFlows.length > 0) {
|
|
268
283
|
this.log(`[DEBUG] Sequential flows: ${sequentialFlows.join(', ')}`);
|
|
@@ -363,6 +378,7 @@ class Cloud extends core_1.Command {
|
|
|
363
378
|
const testFormData = await this.testSubmissionService.buildTestFormData({
|
|
364
379
|
androidApiLevel,
|
|
365
380
|
androidDevice,
|
|
381
|
+
androidNoSnapshot,
|
|
366
382
|
appBinaryId: finalBinaryId,
|
|
367
383
|
cliVersion: this.config.version,
|
|
368
384
|
commonRoot,
|
|
@@ -420,8 +436,11 @@ class Cloud extends core_1.Command {
|
|
|
420
436
|
consoleUrl: url,
|
|
421
437
|
status: 'PENDING',
|
|
422
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,
|
|
423
441
|
name: r.test_file_name,
|
|
424
442
|
status: r.status,
|
|
443
|
+
tags: testMetadataMap[r.test_file_name]?.tags || [],
|
|
425
444
|
})),
|
|
426
445
|
uploadId: results[0].test_upload_id,
|
|
427
446
|
};
|
|
@@ -446,7 +465,7 @@ class Cloud extends core_1.Command {
|
|
|
446
465
|
logger: this.log.bind(this),
|
|
447
466
|
quiet,
|
|
448
467
|
uploadId: results[0].test_upload_id,
|
|
449
|
-
})
|
|
468
|
+
}, testMetadataMap)
|
|
450
469
|
.catch(async (error) => {
|
|
451
470
|
if (error instanceof results_polling_service_1.RunFailedError) {
|
|
452
471
|
// Handle failed test run
|
|
@@ -11,4 +11,5 @@ export declare const deviceFlags: {
|
|
|
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
13
|
'maestro-chrome-onboarding': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
'android-no-snapshot': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
14
15
|
};
|
|
@@ -43,4 +43,8 @@ exports.deviceFlags = {
|
|
|
43
43
|
default: false,
|
|
44
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
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
|
+
}),
|
|
46
50
|
};
|
package/dist/constants.d.ts
CHANGED
|
@@ -43,6 +43,7 @@ export declare const flags: {
|
|
|
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
45
|
'maestro-chrome-onboarding': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
46
|
+
'android-no-snapshot': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
46
47
|
'app-binary-id': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
47
48
|
'app-file': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
48
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;
|
|
@@ -53,6 +55,7 @@ class ResultsPollingService {
|
|
|
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
|
};
|
|
@@ -219,12 +225,12 @@ class ResultsPollingService {
|
|
|
219
225
|
* @returns Promise resolving to final polling result
|
|
220
226
|
*/
|
|
221
227
|
async handleCompletedTests(updatedResults, options) {
|
|
222
|
-
const { uploadId, consoleUrl, json, debug, logger } = options;
|
|
228
|
+
const { uploadId, consoleUrl, json, debug, logger, testMetadata } = options;
|
|
223
229
|
if (debug && logger) {
|
|
224
230
|
logger(`[DEBUG] All tests completed, stopping poll`);
|
|
225
231
|
}
|
|
226
232
|
this.displayFinalResults(updatedResults, consoleUrl, json, logger);
|
|
227
|
-
const output = this.buildPollingResult(updatedResults, uploadId, consoleUrl);
|
|
233
|
+
const output = this.buildPollingResult(updatedResults, uploadId, consoleUrl, testMetadata);
|
|
228
234
|
if (output.status === 'FAILED') {
|
|
229
235
|
if (debug && logger) {
|
|
230
236
|
logger(`[DEBUG] Some tests failed, returning failed status`);
|
|
@@ -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, maestroChromeOnboarding, 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,
|
package/oclif.manifest.json
CHANGED
|
@@ -175,6 +175,12 @@
|
|
|
175
175
|
"allowNo": false,
|
|
176
176
|
"type": "boolean"
|
|
177
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
|
+
},
|
|
178
184
|
"env": {
|
|
179
185
|
"char": "e",
|
|
180
186
|
"description": "One or more environment variables to inject into your flows",
|
|
@@ -667,5 +673,5 @@
|
|
|
667
673
|
]
|
|
668
674
|
}
|
|
669
675
|
},
|
|
670
|
-
"version": "4.2.
|
|
676
|
+
"version": "4.2.5"
|
|
671
677
|
}
|