@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.
@@ -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 5-second intervals
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 5-second intervals */
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;
@@ -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 5-second intervals
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 5-second intervals */
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
  };
@@ -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 tags = config?.tags || [];
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 = 5000;
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`);
@@ -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;
@@ -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,
@@ -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.4"
676
+ "version": "4.2.5"
671
677
  }
package/package.json CHANGED
@@ -67,7 +67,7 @@
67
67
  "type": "git",
68
68
  "url": "https://devicecloud.dev"
69
69
  },
70
- "version": "4.2.4",
70
+ "version": "4.2.5",
71
71
  "bugs": {
72
72
  "url": "https://discord.gg/gm3mJwcNw8"
73
73
  },