@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.
- package/dist/commands/cloud.d.ts +4 -0
- package/dist/commands/cloud.js +109 -47
- package/oclif.manifest.json +47 -7
- package/package.json +1 -1
package/dist/commands/cloud.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/commands/cloud.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
214
|
-
? await compressDir(
|
|
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
|
|
234
|
-
|
|
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;
|
package/oclif.manifest.json
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
187
|
+
"version": "0.0.1-alpha.7"
|
|
148
188
|
}
|
package/package.json
CHANGED