@devicecloud.dev/dcd 0.0.1-alpha.6 → 0.0.1-alpha.8

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.
@@ -7,17 +7,20 @@ export default class Cloud extends Command {
7
7
  static description: string;
8
8
  static examples: string[];
9
9
  static flags: {
10
- androidApiLevel: import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
11
- apiKey: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
12
- apiUrl: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
13
- appBinaryId: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
14
- appFile: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
15
- async: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
10
+ 'android-api-level': import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
11
+ 'api-key': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
12
+ 'api-url': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
13
+ 'app-binary-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
14
+ 'app-file': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
15
+ arm64: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
16
+ async: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
16
17
  env: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
17
- excludeTags: import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
18
+ 'exclude-tags': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
18
19
  flows: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
19
- iOSVersion: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
20
- includeTags: import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
20
+ 'google-play': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
21
+ 'include-tags': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
22
+ 'ios-version': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
23
+ orientation: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
21
24
  };
22
25
  run(): Promise<void>;
23
26
  }
@@ -96,36 +96,44 @@ class Cloud extends core_1.Command {
96
96
  static description = `Test a Flow or set of Flows on devicecloud.dev (https://devicecloud.dev)\nProvide your application file and a folder with Maestro flows to run them in parallel on multiple devices in devicecloud.dev\nThe command will block until all analyses have completed`;
97
97
  static examples = ['<%= config.bin %> <%= command.id %>'];
98
98
  static flags = {
99
- androidApiLevel: core_1.Flags.integer({
99
+ 'android-api-level': core_1.Flags.integer({
100
100
  aliases: ['android-api-level'],
101
- description: 'Android API level to run your flow against',
101
+ description: '[Android only] Android API level to run your flow against',
102
102
  options: ['32', '33', '34'],
103
103
  }),
104
- apiKey: core_1.Flags.string({ aliases: ['api-key'], description: 'API key' }),
105
- apiUrl: core_1.Flags.string({
104
+ 'api-key': core_1.Flags.string({
105
+ aliases: ['api-key'],
106
+ description: 'API key',
107
+ }),
108
+ 'api-url': core_1.Flags.string({
106
109
  aliases: ['api-url'],
107
110
  default: 'https://api.devicecloud.dev',
108
111
  description: 'API base URL',
109
112
  hidden: true,
110
113
  }),
111
- appBinaryId: core_1.Flags.string({
114
+ 'app-binary-id': core_1.Flags.string({
112
115
  aliases: ['app-binary-id'],
113
116
  description: 'The ID of the app binary previously uploaded to Maestro Cloud',
114
117
  }),
115
- appFile: core_1.Flags.file({
118
+ 'app-file': core_1.Flags.file({
116
119
  aliases: ['app-file'],
117
120
  description: 'App binary to run your flows against',
118
121
  }),
119
- async: core_1.Flags.boolean({
120
- default: true,
122
+ arm64: core_1.Flags.boolean({
123
+ default: false,
124
+ description: '[Android only] Run your flow against arm64 devices',
125
+ }),
126
+ async: core_1.Flags.string({
127
+ default: 'true',
121
128
  description: 'Wait for the results of the run',
129
+ options: ['true', 'false'],
122
130
  }),
123
131
  env: core_1.Flags.file({
124
132
  char: 'e',
125
133
  description: 'One or more environment variables to inject into your flows',
126
134
  multiple: true,
127
135
  }),
128
- excludeTags: core_1.Flags.string({
136
+ 'exclude-tags': core_1.Flags.string({
129
137
  aliases: ['exclude-tags'],
130
138
  default: [],
131
139
  description: 'Flows which have these tags will be excluded from the run',
@@ -135,22 +143,35 @@ class Cloud extends core_1.Command {
135
143
  flows: core_1.Flags.string({
136
144
  description: 'The path to the flow file or folder containing your flows',
137
145
  }),
138
- iOSVersion: core_1.Flags.string({
139
- aliases: ['ios-version'],
140
- description: 'iOS version to run your flow against',
141
- options: ['16.4', '17.2'],
146
+ 'google-play': core_1.Flags.boolean({
147
+ aliases: ['google-play'],
148
+ default: false,
149
+ description: '[Android only] Run your flow against Google Play devices',
142
150
  }),
143
- includeTags: core_1.Flags.string({
151
+ 'include-tags': core_1.Flags.string({
144
152
  aliases: ['include-tags'],
145
153
  default: [],
146
154
  description: 'Only flows which have these tags will be included in the run',
147
155
  multiple: true,
148
156
  parse: (input) => input.split(','),
149
157
  }),
158
+ 'ios-version': core_1.Flags.string({
159
+ aliases: ['ios-version'],
160
+ description: '[iOS only] iOS version to run your flow against',
161
+ options: ['16.4', '17.2'],
162
+ }),
163
+ orientation: core_1.Flags.string({
164
+ description: '[Android only] The orientation of the device to run your flow against in degrees',
165
+ options: ['0', '90', '180', '270'],
166
+ }),
150
167
  };
151
168
  async run() {
152
169
  const { args, flags } = await this.parse(Cloud);
153
- const { apiKey, apiUrl, appBinaryId, appFile, async, env, excludeTags, flows, includeTags, ...rest } = flags;
170
+ const { 'api-key': apiKey, 'api-url': apiUrl, 'app-binary-id': appBinaryId, 'app-file': appFile, arm64, async, env, 'exclude-tags': excludeTags, flows, 'google-play': googlePlay, 'include-tags': includeTags, orientation, ...rest } = flags;
171
+ if (arm64) {
172
+ (0, cli_ux_1.info)('Contact hello@devicecloud.dev to enquire about arm64 devices');
173
+ (0, cli_ux_1.exit)();
174
+ }
154
175
  const { firstFile, secondFile } = args;
155
176
  let finalBinaryId = appBinaryId;
156
177
  const finalAppFile = appFile ?? firstFile;
@@ -160,25 +181,38 @@ class Cloud extends core_1.Command {
160
181
  throw new Error('You cannot provide both an appBinaryId and a binary file');
161
182
  }
162
183
  flowFile = flows ?? firstFile;
163
- // this.log(
164
- // `you want to run the flow(s) ${flowFile} against the binary with id ${appBinaryId} with the following flags: ${JSON.stringify(
165
- // flags,
166
- // )}`,
167
- // );
168
184
  }
169
- else {
185
+ if (!flowFile) {
186
+ throw new Error('You must provide a flow file');
187
+ }
188
+ let testFileNames = [];
189
+ let continueOnFailure = true;
190
+ let sequentialFlows = [];
191
+ if (!flowFile?.endsWith('.yaml') &&
192
+ !flowFile?.endsWith('.yml') &&
193
+ !flowFile?.endsWith('/')) {
194
+ flowFile += '/';
195
+ }
196
+ try {
197
+ const executionPlan = await (0, plan_1.plan)(flowFile, includeTags.flat(), excludeTags.flat());
198
+ testFileNames = executionPlan.flowsToRun;
199
+ continueOnFailure = executionPlan.sequence?.continueOnFailure ?? true;
200
+ sequentialFlows = executionPlan.sequence?.flows ?? [];
201
+ }
202
+ catch (error) {
203
+ console.error(error);
204
+ }
205
+ if (!appBinaryId) {
170
206
  if (!(flowFile && finalAppFile)) {
171
207
  throw new Error('You must provide a flow file and an app binary id');
172
208
  }
173
209
  if (!finalAppFile.endsWith('.apk') && !finalAppFile.endsWith('.zip')) {
174
210
  throw new Error('App file must be a .apk or .zip file');
175
211
  }
176
- this.log(`you want to run the flow(s) ${flowFile} against the app ${finalAppFile} with the following flags: ${JSON.stringify(flags)}`);
177
- }
178
- if (!flowFile) {
179
- throw new Error('You must provide a flow file');
212
+ this.log(`you want to run the flow(s) ${flowFile} against the app ${finalAppFile} with the following flags: ${JSON.stringify(flags)}\n`);
180
213
  }
181
214
  if (!finalBinaryId) {
215
+ core_1.ux.action.start('Uploading binary', 'Initializing', { stdout: true });
182
216
  const binaryFormData = new FormData();
183
217
  const binaryBlob = new Blob([await (0, promises_1.readFile)(finalAppFile)], {
184
218
  type: mimeTypeLookupByExtension[finalAppFile.split('.').pop()],
@@ -188,34 +222,13 @@ class Cloud extends core_1.Command {
188
222
  body: binaryFormData,
189
223
  headers: { 'x-app-api-key': apiKey },
190
224
  };
225
+ core_1.ux.action.status = `Uploading`;
191
226
  const { binaryId, message } = await typeSafePost(apiUrl, '/uploads/binary', options);
192
227
  if (!binaryId)
193
228
  throw new Error(message);
194
- this.log(message);
229
+ core_1.ux.action.stop(`\nBinary uploaded with id: ${binaryId}`);
195
230
  finalBinaryId = binaryId;
196
231
  }
197
- const testFileNames = [];
198
- let flowFileDirectory = flowFile;
199
- if (!flowFile?.endsWith('.yaml') && !flowFile?.endsWith('.yml')) {
200
- try {
201
- const executionPlan = await (0, plan_1.plan)(flowFile, includeTags.flat(), excludeTags.flat());
202
- for (const file of executionPlan.flowsToRun) {
203
- testFileNames.push(file);
204
- }
205
- for (const file of executionPlan.sequence?.flows ?? []) {
206
- // todo: handle continueOnFailure and other sequence properties
207
- testFileNames.push(file);
208
- }
209
- }
210
- catch (error) {
211
- console.error(error);
212
- }
213
- }
214
- else {
215
- // we are working with a single file
216
- flowFileDirectory = flowFile.split('/').slice(0, -1).join('/');
217
- testFileNames.push(flowFile.split('/').pop());
218
- }
219
232
  const testFormData = new FormData();
220
233
  // eslint-disable-next-line unicorn/no-array-reduce
221
234
  const envObject = (env ?? []).reduce((acc, cur) => {
@@ -223,8 +236,8 @@ class Cloud extends core_1.Command {
223
236
  acc[key] = value;
224
237
  return acc;
225
238
  }, {});
226
- const buffer = flowFileDirectory?.length
227
- ? await compressDir(flowFileDirectory)
239
+ const buffer = testFileNames?.length > 1
240
+ ? await compressDir(flowFile.split('/').slice(0, -1).join('/'))
228
241
  : await compressFile(flowFile);
229
242
  const blob = new Blob([buffer], {
230
243
  type: mimeTypeLookupByExtension.zip,
@@ -232,7 +245,10 @@ class Cloud extends core_1.Command {
232
245
  testFormData.set('file', blob, 'flowFile.zip');
233
246
  testFormData.set('appBinaryId', finalBinaryId);
234
247
  testFormData.set('testFileNames', JSON.stringify(testFileNames));
248
+ testFormData.set('sequentialFlows', JSON.stringify(sequentialFlows));
235
249
  testFormData.set('env', JSON.stringify(envObject));
250
+ testFormData.set('googlePlay', googlePlay ? 'true' : 'false');
251
+ testFormData.set('config', JSON.stringify({ continueOnFailure, orientation }));
236
252
  for (const [key, value] of Object.entries(rest)) {
237
253
  if (value) {
238
254
  testFormData.set(key, value);
@@ -250,37 +266,37 @@ class Cloud extends core_1.Command {
250
266
  .join(', ')}\n`);
251
267
  (0, cli_ux_1.info)('Run triggered, you can access the results at:');
252
268
  (0, cli_ux_1.info)(`https://console.devicecloud.dev/results/${results[0].test_upload_id}/${results[0].id}/\n`);
253
- if (!async) {
254
- (0, cli_ux_1.info)('completed');
269
+ if (async === 'false') {
270
+ (0, cli_ux_1.info)('Not waiting for results as async flag is set to false');
255
271
  (0, cli_ux_1.exit)(0);
256
272
  }
257
- if (async) {
258
- // poll for the run status every 5 seconds
259
- core_1.ux.action.start('Waiting for results', 'Initializing', { stdout: true });
260
- (0, cli_ux_1.info)(' ');
261
- const intervalId = setInterval(async () => {
262
- const { results: updatedResults } = await typeSafeGet(apiUrl, `/results/${results[0].test_upload_id}`, {
263
- headers: { 'x-app-api-key': apiKey },
264
- });
265
- if (!updatedResults) {
266
- clearInterval(intervalId);
267
- (0, errors_1.error)('No results found');
268
- }
269
- core_1.ux.action.status = `\nStatus | Test
270
- ────────── ─────────── `;
271
- for (const { status, test_file_name: test } of updatedResults) {
272
- core_1.ux.action.status += `\n${status.padEnd(10, ' ')} | ${test}`;
273
- }
274
- if (updatedResults.every((result) => !['PENDING', 'RUNNING'].includes(result.status))) {
275
- (0, cli_ux_1.info)('\nRun completed\n');
276
- (0, cli_ux_1.table)(updatedResults, {
277
- status: { get: (row) => row.status },
278
- test: { get: (row) => row.test_file_name },
279
- }, { printLine: this.log.bind(this) });
280
- clearInterval(intervalId);
281
- }
282
- }, 5000);
283
- }
273
+ // poll for the run status every 5 seconds
274
+ core_1.ux.action.start('Waiting for results', 'Initializing', { stdout: true });
275
+ (0, cli_ux_1.info)('You can safely close this terminal and the tests will continue\n');
276
+ const intervalId = setInterval(async () => {
277
+ const { results: updatedResults } = await typeSafeGet(apiUrl, `/results/${results[0].test_upload_id}`, {
278
+ headers: { 'x-app-api-key': apiKey },
279
+ });
280
+ if (!updatedResults) {
281
+ clearInterval(intervalId);
282
+ (0, errors_1.error)('No results found');
283
+ }
284
+ core_1.ux.action.status = `\nStatus | Test
285
+ ────────── ─────────── `;
286
+ for (const { status, test_file_name: test } of updatedResults) {
287
+ core_1.ux.action.status += `\n${status.padEnd(10, ' ')} | ${test}`;
288
+ }
289
+ if (updatedResults.every((result) => !['PENDING', 'RUNNING'].includes(result.status))) {
290
+ core_1.ux.action.stop('completed');
291
+ (0, cli_ux_1.info)('\n');
292
+ (0, cli_ux_1.table)(updatedResults, {
293
+ status: { get: (row) => row.status },
294
+ test: { get: (row) => row.test_file_name },
295
+ }, { printLine: this.log.bind(this) });
296
+ (0, cli_ux_1.info)('\n');
297
+ clearInterval(intervalId);
298
+ }
299
+ }, 5000);
284
300
  }
285
301
  }
286
302
  exports.default = Cloud;
@@ -19,12 +19,12 @@
19
19
  "<%= config.bin %> <%= command.id %>"
20
20
  ],
21
21
  "flags": {
22
- "androidApiLevel": {
22
+ "android-api-level": {
23
23
  "aliases": [
24
24
  "android-api-level"
25
25
  ],
26
- "description": "Android API level to run your flow against",
27
- "name": "androidApiLevel",
26
+ "description": "[Android only] Android API level to run your flow against",
27
+ "name": "android-api-level",
28
28
  "hasDynamicHelp": false,
29
29
  "multiple": false,
30
30
  "options": [
@@ -34,53 +34,65 @@
34
34
  ],
35
35
  "type": "option"
36
36
  },
37
- "apiKey": {
37
+ "api-key": {
38
38
  "aliases": [
39
39
  "api-key"
40
40
  ],
41
41
  "description": "API key",
42
- "name": "apiKey",
42
+ "name": "api-key",
43
43
  "hasDynamicHelp": false,
44
44
  "multiple": false,
45
45
  "type": "option"
46
46
  },
47
- "apiUrl": {
47
+ "api-url": {
48
48
  "aliases": [
49
49
  "api-url"
50
50
  ],
51
51
  "description": "API base URL",
52
52
  "hidden": true,
53
- "name": "apiUrl",
53
+ "name": "api-url",
54
54
  "default": "https://api.devicecloud.dev",
55
55
  "hasDynamicHelp": false,
56
56
  "multiple": false,
57
57
  "type": "option"
58
58
  },
59
- "appBinaryId": {
59
+ "app-binary-id": {
60
60
  "aliases": [
61
61
  "app-binary-id"
62
62
  ],
63
63
  "description": "The ID of the app binary previously uploaded to Maestro Cloud",
64
- "name": "appBinaryId",
64
+ "name": "app-binary-id",
65
65
  "hasDynamicHelp": false,
66
66
  "multiple": false,
67
67
  "type": "option"
68
68
  },
69
- "appFile": {
69
+ "app-file": {
70
70
  "aliases": [
71
71
  "app-file"
72
72
  ],
73
73
  "description": "App binary to run your flows against",
74
- "name": "appFile",
74
+ "name": "app-file",
75
75
  "hasDynamicHelp": false,
76
76
  "multiple": false,
77
77
  "type": "option"
78
78
  },
79
+ "arm64": {
80
+ "description": "[Android only] Run your flow against arm64 devices",
81
+ "name": "arm64",
82
+ "allowNo": false,
83
+ "type": "boolean"
84
+ },
79
85
  "async": {
80
86
  "description": "Wait for the results of the run",
81
87
  "name": "async",
82
- "allowNo": false,
83
- "type": "boolean"
88
+ "default": "true",
89
+ "hasDynamicHelp": false,
90
+ "multiple": false,
91
+ "options": [
92
+ "true",
93
+ "false"
94
+ ],
95
+ "type": "option"
84
96
  },
85
97
  "env": {
86
98
  "char": "e",
@@ -90,12 +102,12 @@
90
102
  "multiple": true,
91
103
  "type": "option"
92
104
  },
93
- "excludeTags": {
105
+ "exclude-tags": {
94
106
  "aliases": [
95
107
  "exclude-tags"
96
108
  ],
97
109
  "description": "Flows which have these tags will be excluded from the run",
98
- "name": "excludeTags",
110
+ "name": "exclude-tags",
99
111
  "default": [],
100
112
  "hasDynamicHelp": false,
101
113
  "multiple": true,
@@ -108,12 +120,32 @@
108
120
  "multiple": false,
109
121
  "type": "option"
110
122
  },
111
- "iOSVersion": {
123
+ "google-play": {
124
+ "aliases": [
125
+ "google-play"
126
+ ],
127
+ "description": "[Android only] Run your flow against Google Play devices",
128
+ "name": "google-play",
129
+ "allowNo": false,
130
+ "type": "boolean"
131
+ },
132
+ "include-tags": {
133
+ "aliases": [
134
+ "include-tags"
135
+ ],
136
+ "description": "Only flows which have these tags will be included in the run",
137
+ "name": "include-tags",
138
+ "default": [],
139
+ "hasDynamicHelp": false,
140
+ "multiple": true,
141
+ "type": "option"
142
+ },
143
+ "ios-version": {
112
144
  "aliases": [
113
145
  "ios-version"
114
146
  ],
115
- "description": "iOS version to run your flow against",
116
- "name": "iOSVersion",
147
+ "description": "[iOS only] iOS version to run your flow against",
148
+ "name": "ios-version",
117
149
  "hasDynamicHelp": false,
118
150
  "multiple": false,
119
151
  "options": [
@@ -122,15 +154,17 @@
122
154
  ],
123
155
  "type": "option"
124
156
  },
125
- "includeTags": {
126
- "aliases": [
127
- "include-tags"
128
- ],
129
- "description": "Only flows which have these tags will be included in the run",
130
- "name": "includeTags",
131
- "default": [],
157
+ "orientation": {
158
+ "description": "[Android only] The orientation of the device to run your flow against in degrees",
159
+ "name": "orientation",
132
160
  "hasDynamicHelp": false,
133
- "multiple": true,
161
+ "multiple": false,
162
+ "options": [
163
+ "0",
164
+ "90",
165
+ "180",
166
+ "270"
167
+ ],
134
168
  "type": "option"
135
169
  }
136
170
  },
@@ -150,5 +184,5 @@
150
184
  ]
151
185
  }
152
186
  },
153
- "version": "0.0.1-alpha.6"
187
+ "version": "0.0.1-alpha.8"
154
188
  }
package/package.json CHANGED
@@ -65,6 +65,7 @@
65
65
  "url": "@devicecloud.dev/dcd"
66
66
  },
67
67
  "scripts": {
68
+ "dcd": "./bin/dev.js",
68
69
  "build": "shx rm -rf dist && tsc -b",
69
70
  "lint": "eslint . --ext .ts",
70
71
  "postpack": "shx rm -f oclif.manifest.json",
@@ -74,7 +75,7 @@
74
75
  "test": "mocha --forbid-only \"test/**/*.test.ts\"",
75
76
  "version": "oclif readme && git add README.md"
76
77
  },
77
- "version": "0.0.1-alpha.6",
78
+ "version": "0.0.1-alpha.8",
78
79
  "bugs": {
79
80
  "url": "https://discord.gg/GzZBHcUJ"
80
81
  },