@devicecloud.dev/dcd 0.0.1-alpha.5 → 0.0.1-alpha.7

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