@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.
@@ -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.
@@ -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 5-second intervals */
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;
@@ -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;
@@ -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 in degrees',
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: 'Quieter console output that wont provide progress updates',
54
+ description: "Quieter console output that won't provide progress updates",
55
55
  }),
56
56
  report: core_1.Flags.string({
57
57
  aliases: ['format'],
@@ -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 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;
@@ -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
- : styling_1.colors.warning(status.padEnd(10, ' '));
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));
@@ -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;
@@ -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
  }
@@ -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 in degrees",
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 wont provide progress updates",
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.3"
676
+ "version": "4.2.5"
665
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.3",
70
+ "version": "4.2.5",
71
71
  "bugs": {
72
72
  "url": "https://discord.gg/gm3mJwcNw8"
73
73
  },