@devicecloud.dev/dcd 0.0.3 → 0.0.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.
- package/dist/commands/cloud.d.ts +17 -0
- package/dist/commands/cloud.js +62 -238
- package/dist/constants.d.ts +23 -0
- package/dist/constants.js +121 -0
- package/dist/methods.d.ts +16 -0
- package/dist/methods.js +112 -0
- package/dist/plan.d.ts +3 -0
- package/dist/plan.js +64 -82
- package/dist/planMethods.d.ts +28 -0
- package/dist/planMethods.js +135 -0
- package/oclif.manifest.json +2 -2
- package/package.json +4 -3
package/dist/commands/cloud.d.ts
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
+
export declare enum EiOSDevices {
|
|
3
|
+
'ipad-pro-6th-gen' = "ipad-pro-6th-gen",
|
|
4
|
+
'iphone-12' = "iphone-12",
|
|
5
|
+
'iphone-12-mini' = "iphone-12-mini",
|
|
6
|
+
'iphone-12-pro-max' = "iphone-12-pro-max",
|
|
7
|
+
'iphone-13' = "iphone-13",
|
|
8
|
+
'iphone-13-mini' = "iphone-13-mini",
|
|
9
|
+
'iphone-13-pro-max' = "iphone-13-pro-max",
|
|
10
|
+
'iphone-14' = "iphone-14",
|
|
11
|
+
'iphone-14-plus' = "iphone-14-plus",
|
|
12
|
+
'iphone-14-pro' = "iphone-14-pro",
|
|
13
|
+
'iphone-14-pro-max' = "iphone-14-pro-max",
|
|
14
|
+
'iphone-15' = "iphone-15",
|
|
15
|
+
'iphone-15-plus' = "iphone-15-plus",
|
|
16
|
+
'iphone-15-pro' = "iphone-15-pro",
|
|
17
|
+
'iphone-15-pro-max' = "iphone-15-pro-max"
|
|
18
|
+
}
|
|
2
19
|
export default class Cloud extends Command {
|
|
3
20
|
static args: {
|
|
4
21
|
firstFile: import("@oclif/core/lib/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
package/dist/commands/cloud.js
CHANGED
|
@@ -1,91 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EiOSDevices = void 0;
|
|
3
4
|
/* eslint-disable complexity */
|
|
4
5
|
const core_1 = require("@oclif/core");
|
|
5
6
|
const cli_ux_1 = require("@oclif/core/lib/cli-ux");
|
|
6
7
|
const errors_1 = require("@oclif/core/lib/errors");
|
|
7
|
-
const archiver = require("archiver");
|
|
8
8
|
const promises_1 = require("node:fs/promises");
|
|
9
|
-
const
|
|
9
|
+
const path = require("node:path");
|
|
10
|
+
const constants_1 = require("../constants");
|
|
11
|
+
const methods_1 = require("../methods");
|
|
10
12
|
const plan_1 = require("../plan");
|
|
11
13
|
const mimeTypeLookupByExtension = {
|
|
12
14
|
apk: 'application/vnd.android.package-archive',
|
|
13
15
|
yaml: 'application/x-yaml',
|
|
14
16
|
zip: 'application/zip',
|
|
15
17
|
};
|
|
16
|
-
const PERMITTED_EXTENSIONS = new Set([
|
|
17
|
-
'yml',
|
|
18
|
-
'yaml',
|
|
19
|
-
'png',
|
|
20
|
-
'jpg',
|
|
21
|
-
'jpeg',
|
|
22
|
-
'gif',
|
|
23
|
-
'mp4',
|
|
24
|
-
'js',
|
|
25
|
-
]);
|
|
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) => {
|
|
34
|
-
const res = await fetch(baseUrl + path, init);
|
|
35
|
-
if (!res.ok) {
|
|
36
|
-
throw new Error(await res.text());
|
|
37
|
-
}
|
|
38
|
-
return res.json();
|
|
39
|
-
};
|
|
40
|
-
const toBuffer = async (archive) => {
|
|
41
|
-
const chunks = [];
|
|
42
|
-
const writable = new node_stream_1.Writable();
|
|
43
|
-
writable._write = (chunk, _, callback) => {
|
|
44
|
-
// save to array to concatenate later
|
|
45
|
-
chunks.push(chunk);
|
|
46
|
-
callback();
|
|
47
|
-
};
|
|
48
|
-
// pipe to writable
|
|
49
|
-
archive.pipe(writable);
|
|
50
|
-
await archive.finalize();
|
|
51
|
-
// once done, concatenate chunks
|
|
52
|
-
return Buffer.concat(chunks);
|
|
53
|
-
};
|
|
54
|
-
const compressDir = async (sourceDir) => {
|
|
55
|
-
// const output = createWriteStream(zipTargetPath);
|
|
56
|
-
const archive = archiver('zip', {
|
|
57
|
-
zlib: { level: 9 },
|
|
58
|
-
});
|
|
59
|
-
archive.on('error', (err) => {
|
|
60
|
-
throw err;
|
|
61
|
-
});
|
|
62
|
-
archive.directory(sourceDir, '.', (data) => {
|
|
63
|
-
if (data.name.split('/')[0] === 'node_modules')
|
|
64
|
-
return false;
|
|
65
|
-
if (PERMITTED_EXTENSIONS.has(data.name.split('.').pop())) {
|
|
66
|
-
return data;
|
|
67
|
-
}
|
|
68
|
-
return false;
|
|
69
|
-
});
|
|
70
|
-
const buffer = await toBuffer(archive);
|
|
71
|
-
// await writeFile('./my-zip.zip', buffer);
|
|
72
|
-
return buffer;
|
|
73
|
-
};
|
|
74
|
-
// const compressFile = async (sourceFile: string) => {
|
|
75
|
-
// const archive = archiver('zip', {
|
|
76
|
-
// zlib: { level: 9 },
|
|
77
|
-
// });
|
|
78
|
-
// archive.on('error', (err) => {
|
|
79
|
-
// throw err;
|
|
80
|
-
// });
|
|
81
|
-
// console.log(sourceFile, sourceFile.split('/').pop());
|
|
82
|
-
// archive.file(sourceFile.includes('/') ? sourceFile : './' + sourceFile, {
|
|
83
|
-
// name: sourceFile.split('/').pop()!,
|
|
84
|
-
// });
|
|
85
|
-
// const buffer = await toBuffer(archive);
|
|
86
|
-
// await writeFile('./my-file-zip.zip', buffer);
|
|
87
|
-
// return buffer;
|
|
88
|
-
// };
|
|
89
18
|
var EiOSDevices;
|
|
90
19
|
(function (EiOSDevices) {
|
|
91
20
|
EiOSDevices["ipad-pro-6th-gen"] = "ipad-pro-6th-gen";
|
|
@@ -103,11 +32,11 @@ var EiOSDevices;
|
|
|
103
32
|
EiOSDevices["iphone-15-plus"] = "iphone-15-plus";
|
|
104
33
|
EiOSDevices["iphone-15-pro"] = "iphone-15-pro";
|
|
105
34
|
EiOSDevices["iphone-15-pro-max"] = "iphone-15-pro-max";
|
|
106
|
-
})(EiOSDevices || (EiOSDevices = {}));
|
|
35
|
+
})(EiOSDevices || (exports.EiOSDevices = EiOSDevices = {}));
|
|
107
36
|
class Cloud extends core_1.Command {
|
|
108
37
|
static args = {
|
|
109
38
|
firstFile: core_1.Args.string({
|
|
110
|
-
description: 'The binary file of the app to run your flow against, e.g.
|
|
39
|
+
description: 'The binary file of the app to run your flow against, e.g. test.apk for android or test.app/.zip for ios',
|
|
111
40
|
hidden: true,
|
|
112
41
|
name: 'App file',
|
|
113
42
|
}),
|
|
@@ -119,109 +48,10 @@ class Cloud extends core_1.Command {
|
|
|
119
48
|
};
|
|
120
49
|
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`;
|
|
121
50
|
static examples = ['<%= config.bin %> <%= command.id %>'];
|
|
122
|
-
static flags =
|
|
123
|
-
'android-api-level': core_1.Flags.integer({
|
|
124
|
-
description: '[Android only] Android API level to run your flow against',
|
|
125
|
-
options: ['32', '33', '34'],
|
|
126
|
-
}),
|
|
127
|
-
'android-device': core_1.Flags.string({
|
|
128
|
-
description: '[Android only] Android device to run your flow against',
|
|
129
|
-
options: [
|
|
130
|
-
'pixel-6',
|
|
131
|
-
'pixel-6a',
|
|
132
|
-
'pixel-6-pro',
|
|
133
|
-
'pixel-7',
|
|
134
|
-
'pixel-7-pro',
|
|
135
|
-
'generic-tablet',
|
|
136
|
-
],
|
|
137
|
-
}),
|
|
138
|
-
apiKey: core_1.Flags.string({
|
|
139
|
-
aliases: ['api-key'],
|
|
140
|
-
description: 'API key',
|
|
141
|
-
}),
|
|
142
|
-
apiUrl: core_1.Flags.string({
|
|
143
|
-
aliases: ['api-url', 'apiURL'],
|
|
144
|
-
default: 'https://api.devicecloud.dev',
|
|
145
|
-
description: 'API base URL',
|
|
146
|
-
hidden: true,
|
|
147
|
-
}),
|
|
148
|
-
'app-binary-id': core_1.Flags.string({
|
|
149
|
-
aliases: ['app-binary-id'],
|
|
150
|
-
description: 'The ID of the app binary previously uploaded to Maestro Cloud',
|
|
151
|
-
}),
|
|
152
|
-
'app-file': core_1.Flags.file({
|
|
153
|
-
aliases: ['app-file'],
|
|
154
|
-
description: 'App binary to run your flows against',
|
|
155
|
-
}),
|
|
156
|
-
arm64: core_1.Flags.boolean({
|
|
157
|
-
default: false,
|
|
158
|
-
description: '[Android only] Run your flow against arm64 devices',
|
|
159
|
-
}),
|
|
160
|
-
async: core_1.Flags.boolean({
|
|
161
|
-
description: 'Wait for the results of the run',
|
|
162
|
-
}),
|
|
163
|
-
env: core_1.Flags.file({
|
|
164
|
-
char: 'e',
|
|
165
|
-
description: 'One or more environment variables to inject into your flows',
|
|
166
|
-
multiple: true,
|
|
167
|
-
}),
|
|
168
|
-
'exclude-tags': core_1.Flags.string({
|
|
169
|
-
aliases: ['exclude-tags'],
|
|
170
|
-
default: [],
|
|
171
|
-
description: 'Flows which have these tags will be excluded from the run',
|
|
172
|
-
multiple: true,
|
|
173
|
-
parse: (input) => input.split(','),
|
|
174
|
-
}),
|
|
175
|
-
flows: core_1.Flags.string({
|
|
176
|
-
description: 'The path to the flow file or folder containing your flows',
|
|
177
|
-
}),
|
|
178
|
-
'google-play': core_1.Flags.boolean({
|
|
179
|
-
aliases: ['google-play'],
|
|
180
|
-
default: false,
|
|
181
|
-
description: '[Android only] Run your flow against Google Play devices',
|
|
182
|
-
}),
|
|
183
|
-
'include-tags': core_1.Flags.string({
|
|
184
|
-
aliases: ['include-tags'],
|
|
185
|
-
default: [],
|
|
186
|
-
description: 'Only flows which have these tags will be included in the run',
|
|
187
|
-
multiple: true,
|
|
188
|
-
parse: (input) => input.split(','),
|
|
189
|
-
}),
|
|
190
|
-
'ios-device': core_1.Flags.string({
|
|
191
|
-
description: '[iOS only] iOS device to run your flow against',
|
|
192
|
-
options: [
|
|
193
|
-
'iphone-12',
|
|
194
|
-
'iphone-12-mini',
|
|
195
|
-
'iphone-12-pro-max',
|
|
196
|
-
'iphone-13',
|
|
197
|
-
'iphone-13-mini',
|
|
198
|
-
'iphone-13-pro-max',
|
|
199
|
-
'iphone-14',
|
|
200
|
-
'iphone-14-plus',
|
|
201
|
-
'iphone-14-pro',
|
|
202
|
-
'iphone-14-pro-max',
|
|
203
|
-
'iphone-15',
|
|
204
|
-
'iphone-15-plus',
|
|
205
|
-
'iphone-15-pro',
|
|
206
|
-
'iphone-15-pro-max',
|
|
207
|
-
'ipad-pro-6th-gen',
|
|
208
|
-
],
|
|
209
|
-
}),
|
|
210
|
-
'ios-version': core_1.Flags.string({
|
|
211
|
-
description: '[iOS only] iOS version to run your flow against',
|
|
212
|
-
options: ['15', '16', '17'],
|
|
213
|
-
}),
|
|
214
|
-
name: core_1.Flags.string({
|
|
215
|
-
description: 'A custom name for your upload (useful for tagging commits etc)',
|
|
216
|
-
}),
|
|
217
|
-
orientation: core_1.Flags.string({
|
|
218
|
-
description: '[Android only] The orientation of the device to run your flow against in degrees',
|
|
219
|
-
options: ['0', '90', '180', '270'],
|
|
220
|
-
}),
|
|
221
|
-
};
|
|
51
|
+
static flags = constants_1.flags;
|
|
222
52
|
async run() {
|
|
223
53
|
try {
|
|
224
|
-
const { args, flags } = await this.parse(Cloud);
|
|
54
|
+
const { args, flags, raw } = await this.parse(Cloud);
|
|
225
55
|
const { 'android-api-level': androidApiLevel, 'android-device': androidDevice, apiKey, apiUrl, 'app-binary-id': appBinaryId, 'app-file': appFile, arm64, async, env, 'exclude-tags': excludeTags, flows, 'google-play': googlePlay, 'include-tags': includeTags, 'ios-device': iOSDevice, 'ios-version': iOSVersion, name, orientation, ...rest } = flags;
|
|
226
56
|
if (arm64) {
|
|
227
57
|
(0, cli_ux_1.info)('Contact hello@devicecloud.dev to enquire about arm64 devices');
|
|
@@ -241,52 +71,38 @@ class Cloud extends core_1.Command {
|
|
|
241
71
|
throw new Error('You must provide a flow file');
|
|
242
72
|
}
|
|
243
73
|
if (iOSVersion) {
|
|
244
|
-
const iOSCompatibilityLookup = {
|
|
245
|
-
'ipad-pro-6th-gen': ['16', '17'],
|
|
246
|
-
'iphone-12': ['15', '16', '17'],
|
|
247
|
-
'iphone-12-mini': ['15', '16', '17'],
|
|
248
|
-
'iphone-12-pro-max': ['15', '16', '17'],
|
|
249
|
-
'iphone-13': ['15', '16', '17'],
|
|
250
|
-
'iphone-13-mini': ['15', '16', '17'],
|
|
251
|
-
'iphone-13-pro-max': ['15', '16', '17'],
|
|
252
|
-
'iphone-14': ['16', '17'],
|
|
253
|
-
'iphone-14-plus': ['16', '17'],
|
|
254
|
-
'iphone-14-pro': ['16', '17'],
|
|
255
|
-
'iphone-14-pro-max': ['16', '17'],
|
|
256
|
-
'iphone-15': ['17'],
|
|
257
|
-
'iphone-15-plus': ['17'],
|
|
258
|
-
'iphone-15-pro': ['17'],
|
|
259
|
-
'iphone-15-pro-max': ['17'],
|
|
260
|
-
};
|
|
261
74
|
const iOSDeviceID = iOSDevice || 'iphone-14';
|
|
262
|
-
const supportediOSVersions = iOSCompatibilityLookup[iOSDeviceID];
|
|
75
|
+
const supportediOSVersions = constants_1.iOSCompatibilityLookup[iOSDeviceID];
|
|
263
76
|
if (!supportediOSVersions.includes(iOSVersion)) {
|
|
264
77
|
throw new Error(`${iOSDeviceID} only supports these iOS versions: ${supportediOSVersions.join(',')}`);
|
|
265
78
|
}
|
|
266
79
|
}
|
|
267
|
-
|
|
268
|
-
let continueOnFailure = true;
|
|
269
|
-
let sequentialFlows = [];
|
|
80
|
+
flowFile = path.resolve(flowFile);
|
|
270
81
|
if (!flowFile?.endsWith('.yaml') &&
|
|
271
82
|
!flowFile?.endsWith('.yml') &&
|
|
272
83
|
!flowFile?.endsWith('/')) {
|
|
273
84
|
flowFile += '/';
|
|
274
85
|
}
|
|
86
|
+
let executionPlan;
|
|
275
87
|
try {
|
|
276
|
-
|
|
277
|
-
testFileNames = executionPlan.flowsToRun;
|
|
278
|
-
continueOnFailure = executionPlan.sequence?.continueOnFailure ?? true;
|
|
279
|
-
sequentialFlows = executionPlan.sequence?.flows ?? [];
|
|
88
|
+
executionPlan = await (0, plan_1.plan)(flowFile, includeTags.flat(), excludeTags.flat());
|
|
280
89
|
}
|
|
281
90
|
catch (error) {
|
|
282
91
|
console.error(error);
|
|
92
|
+
// eslint-disable-next-line no-process-exit, unicorn/no-process-exit
|
|
93
|
+
process.exit(2);
|
|
283
94
|
}
|
|
95
|
+
const { allExcludeTags, allIncludeTags, flowsToRun: testFileNames, referencedFiles, sequence, } = executionPlan;
|
|
96
|
+
const { continueOnFailure = true, flows: sequentialFlows = [] } = sequence ?? {};
|
|
284
97
|
if (!appBinaryId) {
|
|
285
98
|
if (!(flowFile && finalAppFile)) {
|
|
286
99
|
throw new Error('You must provide a flow file and an app binary id');
|
|
287
100
|
}
|
|
288
|
-
if (!
|
|
289
|
-
throw new Error('App file must be a .apk or .zip file');
|
|
101
|
+
if (!['apk', '.app', '.zip'].some((ext) => finalAppFile.endsWith(ext))) {
|
|
102
|
+
throw new Error('App file must be a .apk for android or .app/.zip file for iOS');
|
|
103
|
+
}
|
|
104
|
+
if (finalAppFile.endsWith('.zip')) {
|
|
105
|
+
await (0, methods_1.verifyAppZip)(finalAppFile);
|
|
290
106
|
}
|
|
291
107
|
}
|
|
292
108
|
const flagLogs = [];
|
|
@@ -297,28 +113,34 @@ class Cloud extends core_1.Command {
|
|
|
297
113
|
}
|
|
298
114
|
this.log(`
|
|
299
115
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
116
|
+
Submitting new job
|
|
117
|
+
→ Flow(s): ${flowFile}
|
|
118
|
+
→ App: ${appBinaryId || finalAppFile}
|
|
303
119
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
120
|
+
With options
|
|
121
|
+
→ ${flagLogs.join(`
|
|
122
|
+
→ `)}
|
|
307
123
|
|
|
308
|
-
|
|
124
|
+
`);
|
|
309
125
|
if (!finalBinaryId) {
|
|
310
126
|
core_1.ux.action.start('Uploading binary', 'Initializing', { stdout: true });
|
|
311
127
|
const binaryFormData = new FormData();
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
128
|
+
if (finalAppFile?.endsWith('.app')) {
|
|
129
|
+
const zippedAppBlob = await (0, methods_1.compressFolderToBlob)(finalAppFile);
|
|
130
|
+
binaryFormData.set('file', zippedAppBlob, finalAppFile + '.zip');
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const binaryBlob = new Blob([await (0, promises_1.readFile)(finalAppFile)], {
|
|
134
|
+
type: mimeTypeLookupByExtension[finalAppFile.split('.').pop()],
|
|
135
|
+
});
|
|
136
|
+
binaryFormData.set('file', binaryBlob, finalAppFile);
|
|
137
|
+
}
|
|
316
138
|
const options = {
|
|
317
139
|
body: binaryFormData,
|
|
318
140
|
headers: { 'x-app-api-key': apiKey },
|
|
319
141
|
};
|
|
320
142
|
core_1.ux.action.status = `Uploading`;
|
|
321
|
-
const { binaryId, message } = await typeSafePost(apiUrl, '/uploads/binary', options);
|
|
143
|
+
const { binaryId, message } = await (0, methods_1.typeSafePost)(apiUrl, '/uploads/binary', options);
|
|
322
144
|
if (!binaryId)
|
|
323
145
|
throw new Error(message);
|
|
324
146
|
core_1.ux.action.stop(`\nBinary uploaded with id: ${binaryId}`);
|
|
@@ -331,14 +153,15 @@ class Cloud extends core_1.Command {
|
|
|
331
153
|
acc[key] = value;
|
|
332
154
|
return acc;
|
|
333
155
|
}, {});
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
156
|
+
const buffer = await (0, methods_1.compressFilesFromRelativePath)(flowFile?.endsWith('.yaml') || flowFile?.endsWith('.yml')
|
|
157
|
+
? path.dirname(flowFile)
|
|
158
|
+
: flowFile, [
|
|
159
|
+
...new Set([
|
|
160
|
+
...referencedFiles,
|
|
161
|
+
...testFileNames,
|
|
162
|
+
...sequentialFlows,
|
|
163
|
+
]),
|
|
164
|
+
]);
|
|
342
165
|
const blob = new Blob([buffer], {
|
|
343
166
|
type: mimeTypeLookupByExtension.zip,
|
|
344
167
|
});
|
|
@@ -348,22 +171,23 @@ class Cloud extends core_1.Command {
|
|
|
348
171
|
testFormData.set('sequentialFlows', JSON.stringify(sequentialFlows));
|
|
349
172
|
testFormData.set('env', JSON.stringify(envObject));
|
|
350
173
|
testFormData.set('googlePlay', googlePlay ? 'true' : 'false');
|
|
351
|
-
testFormData.set('config', JSON.stringify({
|
|
352
|
-
|
|
174
|
+
testFormData.set('config', JSON.stringify({
|
|
175
|
+
allExcludeTags,
|
|
176
|
+
allIncludeTags,
|
|
177
|
+
continueOnFailure,
|
|
178
|
+
orientation,
|
|
179
|
+
raw,
|
|
180
|
+
}));
|
|
181
|
+
if (androidApiLevel)
|
|
353
182
|
testFormData.set('androidApiLevel', androidApiLevel.toString());
|
|
354
|
-
|
|
355
|
-
if (androidDevice) {
|
|
183
|
+
if (androidDevice)
|
|
356
184
|
testFormData.set('androidDevice', androidDevice.toString());
|
|
357
|
-
|
|
358
|
-
if (iOSVersion) {
|
|
185
|
+
if (iOSVersion)
|
|
359
186
|
testFormData.set('iOSVersion', iOSVersion.toString());
|
|
360
|
-
|
|
361
|
-
if (iOSDevice) {
|
|
187
|
+
if (iOSDevice)
|
|
362
188
|
testFormData.set('iOSDevice', iOSDevice.toString());
|
|
363
|
-
|
|
364
|
-
if (name) {
|
|
189
|
+
if (name)
|
|
365
190
|
testFormData.set('name', name.toString());
|
|
366
|
-
}
|
|
367
191
|
for (const [key, value] of Object.entries(rest)) {
|
|
368
192
|
if (value) {
|
|
369
193
|
testFormData.set(key, value);
|
|
@@ -373,7 +197,7 @@ class Cloud extends core_1.Command {
|
|
|
373
197
|
body: testFormData,
|
|
374
198
|
headers: { 'x-app-api-key': apiKey },
|
|
375
199
|
};
|
|
376
|
-
const { message, results } = await typeSafePost(apiUrl, '/uploads/flow', options);
|
|
200
|
+
const { message, results } = await (0, methods_1.typeSafePost)(apiUrl, '/uploads/flow', options);
|
|
377
201
|
if (!results?.length)
|
|
378
202
|
(0, errors_1.error)('No tests created: ' + message);
|
|
379
203
|
(0, cli_ux_1.info)(`\nCreated ${results.length} tests: ${results
|
|
@@ -390,7 +214,7 @@ class Cloud extends core_1.Command {
|
|
|
390
214
|
core_1.ux.action.start('Waiting for results', 'Initializing', { stdout: true });
|
|
391
215
|
(0, cli_ux_1.info)('\nYou can safely close this terminal and the tests will continue\n');
|
|
392
216
|
const intervalId = setInterval(async () => {
|
|
393
|
-
const { results: updatedResults } = await typeSafeGet(apiUrl, `/results/${results[0].test_upload_id}`, {
|
|
217
|
+
const { results: updatedResults } = await (0, methods_1.typeSafeGet)(apiUrl, `/results/${results[0].test_upload_id}`, {
|
|
394
218
|
headers: { 'x-app-api-key': apiKey },
|
|
395
219
|
});
|
|
396
220
|
if (!updatedResults) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { EiOSDevices } from './commands/cloud';
|
|
2
|
+
export declare const flags: {
|
|
3
|
+
'android-api-level': import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
4
|
+
'android-device': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
5
|
+
apiKey: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
6
|
+
apiUrl: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
7
|
+
'app-binary-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
8
|
+
'app-file': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
9
|
+
arm64: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
async: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
env: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
12
|
+
'exclude-tags': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
13
|
+
flows: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
14
|
+
'google-play': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
'include-tags': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
16
|
+
'ios-device': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
17
|
+
'ios-version': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
18
|
+
name: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
19
|
+
orientation: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
20
|
+
};
|
|
21
|
+
export declare const iOSCompatibilityLookup: {
|
|
22
|
+
[k in EiOSDevices]: string[];
|
|
23
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.iOSCompatibilityLookup = exports.flags = void 0;
|
|
4
|
+
const core_1 = require("@oclif/core");
|
|
5
|
+
exports.flags = {
|
|
6
|
+
'android-api-level': core_1.Flags.integer({
|
|
7
|
+
description: '[Android only] Android API level to run your flow against',
|
|
8
|
+
options: ['32', '33', '34'],
|
|
9
|
+
}),
|
|
10
|
+
'android-device': core_1.Flags.string({
|
|
11
|
+
description: '[Android only] Android device to run your flow against',
|
|
12
|
+
options: [
|
|
13
|
+
'pixel-6',
|
|
14
|
+
'pixel-6a',
|
|
15
|
+
'pixel-6-pro',
|
|
16
|
+
'pixel-7',
|
|
17
|
+
'pixel-7-pro',
|
|
18
|
+
'generic-tablet',
|
|
19
|
+
],
|
|
20
|
+
}),
|
|
21
|
+
apiKey: core_1.Flags.string({
|
|
22
|
+
aliases: ['api-key'],
|
|
23
|
+
description: 'API key',
|
|
24
|
+
}),
|
|
25
|
+
apiUrl: core_1.Flags.string({
|
|
26
|
+
aliases: ['api-url', 'apiURL'],
|
|
27
|
+
default: 'https://api.devicecloud.dev',
|
|
28
|
+
description: 'API base URL',
|
|
29
|
+
hidden: true,
|
|
30
|
+
}),
|
|
31
|
+
'app-binary-id': core_1.Flags.string({
|
|
32
|
+
aliases: ['app-binary-id'],
|
|
33
|
+
description: 'The ID of the app binary previously uploaded to Maestro Cloud',
|
|
34
|
+
}),
|
|
35
|
+
'app-file': core_1.Flags.file({
|
|
36
|
+
aliases: ['app-file'],
|
|
37
|
+
description: 'App binary to run your flows against',
|
|
38
|
+
}),
|
|
39
|
+
arm64: core_1.Flags.boolean({
|
|
40
|
+
default: false,
|
|
41
|
+
description: '[Android only] Run your flow against arm64 devices',
|
|
42
|
+
}),
|
|
43
|
+
async: core_1.Flags.boolean({
|
|
44
|
+
description: 'Wait for the results of the run',
|
|
45
|
+
}),
|
|
46
|
+
env: core_1.Flags.file({
|
|
47
|
+
char: 'e',
|
|
48
|
+
description: 'One or more environment variables to inject into your flows',
|
|
49
|
+
multiple: true,
|
|
50
|
+
}),
|
|
51
|
+
'exclude-tags': core_1.Flags.string({
|
|
52
|
+
aliases: ['exclude-tags'],
|
|
53
|
+
default: [],
|
|
54
|
+
description: 'Flows which have these tags will be excluded from the run',
|
|
55
|
+
multiple: true,
|
|
56
|
+
parse: (input) => input.split(','),
|
|
57
|
+
}),
|
|
58
|
+
flows: core_1.Flags.string({
|
|
59
|
+
description: 'The path to the flow file or folder containing your flows',
|
|
60
|
+
}),
|
|
61
|
+
'google-play': core_1.Flags.boolean({
|
|
62
|
+
aliases: ['google-play'],
|
|
63
|
+
default: false,
|
|
64
|
+
description: '[Android only] Run your flow against Google Play devices',
|
|
65
|
+
}),
|
|
66
|
+
'include-tags': core_1.Flags.string({
|
|
67
|
+
aliases: ['include-tags'],
|
|
68
|
+
default: [],
|
|
69
|
+
description: 'Only flows which have these tags will be included in the run',
|
|
70
|
+
multiple: true,
|
|
71
|
+
parse: (input) => input.split(','),
|
|
72
|
+
}),
|
|
73
|
+
'ios-device': core_1.Flags.string({
|
|
74
|
+
description: '[iOS only] iOS device to run your flow against',
|
|
75
|
+
options: [
|
|
76
|
+
'iphone-12',
|
|
77
|
+
'iphone-12-mini',
|
|
78
|
+
'iphone-12-pro-max',
|
|
79
|
+
'iphone-13',
|
|
80
|
+
'iphone-13-mini',
|
|
81
|
+
'iphone-13-pro-max',
|
|
82
|
+
'iphone-14',
|
|
83
|
+
'iphone-14-plus',
|
|
84
|
+
'iphone-14-pro',
|
|
85
|
+
'iphone-14-pro-max',
|
|
86
|
+
'iphone-15',
|
|
87
|
+
'iphone-15-plus',
|
|
88
|
+
'iphone-15-pro',
|
|
89
|
+
'iphone-15-pro-max',
|
|
90
|
+
'ipad-pro-6th-gen',
|
|
91
|
+
],
|
|
92
|
+
}),
|
|
93
|
+
'ios-version': core_1.Flags.string({
|
|
94
|
+
description: '[iOS only] iOS version to run your flow against',
|
|
95
|
+
options: ['15', '16', '17'],
|
|
96
|
+
}),
|
|
97
|
+
name: core_1.Flags.string({
|
|
98
|
+
description: 'A custom name for your upload (useful for tagging commits etc)',
|
|
99
|
+
}),
|
|
100
|
+
orientation: core_1.Flags.string({
|
|
101
|
+
description: '[Android only] The orientation of the device to run your flow against in degrees',
|
|
102
|
+
options: ['0', '90', '180', '270'],
|
|
103
|
+
}),
|
|
104
|
+
};
|
|
105
|
+
exports.iOSCompatibilityLookup = {
|
|
106
|
+
'ipad-pro-6th-gen': ['16', '17'],
|
|
107
|
+
'iphone-12': ['15', '16', '17'],
|
|
108
|
+
'iphone-12-mini': ['15', '16', '17'],
|
|
109
|
+
'iphone-12-pro-max': ['15', '16', '17'],
|
|
110
|
+
'iphone-13': ['15', '16', '17'],
|
|
111
|
+
'iphone-13-mini': ['15', '16', '17'],
|
|
112
|
+
'iphone-13-pro-max': ['15', '16', '17'],
|
|
113
|
+
'iphone-14': ['16', '17'],
|
|
114
|
+
'iphone-14-plus': ['16', '17'],
|
|
115
|
+
'iphone-14-pro': ['16', '17'],
|
|
116
|
+
'iphone-14-pro-max': ['16', '17'],
|
|
117
|
+
'iphone-15': ['17'],
|
|
118
|
+
'iphone-15-plus': ['17'],
|
|
119
|
+
'iphone-15-pro': ['17'],
|
|
120
|
+
'iphone-15-pro-max': ['17'],
|
|
121
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import * as archiver from 'archiver';
|
|
3
|
+
import { paths } from '../../api/schema.types';
|
|
4
|
+
export declare const typeSafePost: <T extends keyof paths>(baseUrl: string, path: string, init?: {
|
|
5
|
+
body?: FormData;
|
|
6
|
+
headers?: HeadersInit;
|
|
7
|
+
}) => Promise<paths[T]["post"]["responses"]["201"]["content"]["application/json"]>;
|
|
8
|
+
export declare const typeSafeGet: <T extends keyof paths>(baseUrl: string, path: string, init?: {
|
|
9
|
+
body?: FormData;
|
|
10
|
+
headers?: HeadersInit;
|
|
11
|
+
}) => Promise<paths[T]["get"]["responses"]["200"]["content"]["application/json"]>;
|
|
12
|
+
export declare const toBuffer: (archive: archiver.Archiver) => Promise<Buffer>;
|
|
13
|
+
export declare const compressDir: (sourceDir: string) => Promise<Buffer>;
|
|
14
|
+
export declare const compressFolderToBlob: (sourceDir: string) => Promise<Blob>;
|
|
15
|
+
export declare const compressFilesFromRelativePath: (path: string, files: string[]) => Promise<Buffer>;
|
|
16
|
+
export declare const verifyAppZip: (zipPath: string) => Promise<void>;
|
package/dist/methods.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifyAppZip = exports.compressFilesFromRelativePath = exports.compressFolderToBlob = exports.compressDir = exports.toBuffer = exports.typeSafeGet = exports.typeSafePost = void 0;
|
|
4
|
+
const archiver = require("archiver");
|
|
5
|
+
// import { writeFile } from 'node:fs/promises';
|
|
6
|
+
const nodePath = require("node:path");
|
|
7
|
+
const node_stream_1 = require("node:stream");
|
|
8
|
+
const StreamZip = require("node-stream-zip");
|
|
9
|
+
const PERMITTED_EXTENSIONS = new Set([
|
|
10
|
+
'yml',
|
|
11
|
+
'yaml',
|
|
12
|
+
'png',
|
|
13
|
+
'jpg',
|
|
14
|
+
'jpeg',
|
|
15
|
+
'gif',
|
|
16
|
+
'mp4',
|
|
17
|
+
'js',
|
|
18
|
+
]);
|
|
19
|
+
const typeSafePost = async (baseUrl, path, init) => {
|
|
20
|
+
const res = await fetch(baseUrl + path, { ...init, method: 'POST' });
|
|
21
|
+
if (!res.ok) {
|
|
22
|
+
throw new Error(await res.text());
|
|
23
|
+
}
|
|
24
|
+
return res.json();
|
|
25
|
+
};
|
|
26
|
+
exports.typeSafePost = typeSafePost;
|
|
27
|
+
const typeSafeGet = async (baseUrl, path, init) => {
|
|
28
|
+
const res = await fetch(baseUrl + path, init);
|
|
29
|
+
if (!res.ok) {
|
|
30
|
+
throw new Error(await res.text());
|
|
31
|
+
}
|
|
32
|
+
return res.json();
|
|
33
|
+
};
|
|
34
|
+
exports.typeSafeGet = typeSafeGet;
|
|
35
|
+
const toBuffer = async (archive) => {
|
|
36
|
+
const chunks = [];
|
|
37
|
+
const writable = new node_stream_1.Writable();
|
|
38
|
+
writable._write = (chunk, _, callback) => {
|
|
39
|
+
// save to array to concatenate later
|
|
40
|
+
chunks.push(chunk);
|
|
41
|
+
callback();
|
|
42
|
+
};
|
|
43
|
+
// pipe to writable
|
|
44
|
+
archive.pipe(writable);
|
|
45
|
+
await archive.finalize();
|
|
46
|
+
// once done, concatenate chunks
|
|
47
|
+
return Buffer.concat(chunks);
|
|
48
|
+
};
|
|
49
|
+
exports.toBuffer = toBuffer;
|
|
50
|
+
const compressDir = async (sourceDir) => {
|
|
51
|
+
// const output = createWriteStream(zipTargetPath);
|
|
52
|
+
const archive = archiver('zip', {
|
|
53
|
+
zlib: { level: 9 },
|
|
54
|
+
});
|
|
55
|
+
archive.on('error', (err) => {
|
|
56
|
+
throw err;
|
|
57
|
+
});
|
|
58
|
+
archive.directory(sourceDir, '.', (data) => {
|
|
59
|
+
if (data.name.split('/')[0] === 'node_modules')
|
|
60
|
+
return false;
|
|
61
|
+
if (PERMITTED_EXTENSIONS.has(data.name.split('.').pop())) {
|
|
62
|
+
return data;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
});
|
|
66
|
+
const buffer = await (0, exports.toBuffer)(archive);
|
|
67
|
+
// await writeFile('./my-zip.zip', buffer);
|
|
68
|
+
return buffer;
|
|
69
|
+
};
|
|
70
|
+
exports.compressDir = compressDir;
|
|
71
|
+
const compressFolderToBlob = async (sourceDir) => {
|
|
72
|
+
const archive = archiver('zip', {
|
|
73
|
+
zlib: { level: 9 },
|
|
74
|
+
});
|
|
75
|
+
archive.on('error', (err) => {
|
|
76
|
+
throw err;
|
|
77
|
+
});
|
|
78
|
+
archive.directory(sourceDir, sourceDir.split('/').pop());
|
|
79
|
+
const buffer = await (0, exports.toBuffer)(archive);
|
|
80
|
+
return new Blob([buffer], { type: 'application/zip' });
|
|
81
|
+
};
|
|
82
|
+
exports.compressFolderToBlob = compressFolderToBlob;
|
|
83
|
+
const compressFilesFromRelativePath = async (path, files) => {
|
|
84
|
+
const archive = archiver('zip', {
|
|
85
|
+
zlib: { level: 9 },
|
|
86
|
+
});
|
|
87
|
+
archive.on('error', (err) => {
|
|
88
|
+
throw err;
|
|
89
|
+
});
|
|
90
|
+
for (const file of files) {
|
|
91
|
+
archive.file(nodePath.resolve(path, file), { name: file });
|
|
92
|
+
}
|
|
93
|
+
const buffer = await (0, exports.toBuffer)(archive);
|
|
94
|
+
// await writeFile('./my-zip.zip', buffer);
|
|
95
|
+
return buffer;
|
|
96
|
+
};
|
|
97
|
+
exports.compressFilesFromRelativePath = compressFilesFromRelativePath;
|
|
98
|
+
const verifyAppZip = async (zipPath) => {
|
|
99
|
+
// eslint-disable-next-line import/namespace, new-cap
|
|
100
|
+
const zip = await new StreamZip.async({
|
|
101
|
+
file: zipPath,
|
|
102
|
+
storeEntries: true,
|
|
103
|
+
});
|
|
104
|
+
const entries = await zip.entries();
|
|
105
|
+
const topLevelEntries = Object.values(entries).filter((entry) => !entry.name.split('/')[1]);
|
|
106
|
+
if (topLevelEntries.length !== 1 ||
|
|
107
|
+
!topLevelEntries[0].name.endsWith('.app/')) {
|
|
108
|
+
throw new Error('Zip file must contain exactly one entry which is a .app, check the contents of the zip file');
|
|
109
|
+
}
|
|
110
|
+
zip.close();
|
|
111
|
+
};
|
|
112
|
+
exports.verifyAppZip = verifyAppZip;
|
package/dist/plan.d.ts
CHANGED
package/dist/plan.js
CHANGED
|
@@ -1,76 +1,34 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.plan = void 0;
|
|
4
|
-
const
|
|
5
|
-
const yaml = require("js-yaml");
|
|
4
|
+
const glob_1 = require("glob");
|
|
6
5
|
const fs = require("node:fs");
|
|
7
6
|
const path = require("node:path");
|
|
8
|
-
|
|
9
|
-
if (flowOrder.length === 0)
|
|
10
|
-
return [];
|
|
11
|
-
const orderSet = new Set(flowOrder);
|
|
12
|
-
const namesInOrder = Object.keys(paths).filter((key) => orderSet.has(key));
|
|
13
|
-
if (namesInOrder.length === 0)
|
|
14
|
-
return [];
|
|
15
|
-
const result = [...orderSet].filter((item) => namesInOrder.includes(item));
|
|
16
|
-
if (result.length === 0) {
|
|
17
|
-
throw new Error(`Could not find flows needed for execution in order: ${[...orderSet]
|
|
18
|
-
.filter((item) => !namesInOrder.includes(item))
|
|
19
|
-
.join(', ')}`);
|
|
20
|
-
}
|
|
21
|
-
else if (flowOrder
|
|
22
|
-
.slice(0, result.length)
|
|
23
|
-
.every((value, index) => value === result[index])) {
|
|
24
|
-
return result.map((item) => paths[item]);
|
|
25
|
-
}
|
|
26
|
-
else {
|
|
27
|
-
return [];
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
function isFlowFile(filePath) {
|
|
31
|
-
return filePath.endsWith('.yaml') || filePath.endsWith('.yml');
|
|
32
|
-
}
|
|
33
|
-
const readYamlFileAsJson = (filePath) => {
|
|
34
|
-
const yamlText = fs.readFileSync(filePath, 'utf8');
|
|
35
|
-
return yaml.load(yamlText);
|
|
36
|
-
};
|
|
37
|
-
const readConfigFromYamlFileAsJson = (filePath) => {
|
|
38
|
-
const yamlText = fs.readFileSync(filePath, 'utf8');
|
|
39
|
-
if (yamlText.includes('\n---\n')) {
|
|
40
|
-
const yamlTexts = yamlText.split('\n---\n');
|
|
41
|
-
const config = yaml.load(yamlTexts[0]);
|
|
42
|
-
if (Object.keys(config).length > 0) {
|
|
43
|
-
return config;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return null;
|
|
47
|
-
};
|
|
48
|
-
async function walk(dir, filterFunction) {
|
|
49
|
-
const readDirResult = await fs.promises.readdir(dir);
|
|
50
|
-
const files = await Promise.all(readDirResult.map(async (file) => {
|
|
51
|
-
const filePath = path.join(dir, file);
|
|
52
|
-
const stats = await fs.promises.stat(filePath);
|
|
53
|
-
if (stats.isDirectory())
|
|
54
|
-
return walk(filePath);
|
|
55
|
-
if (stats.isFile())
|
|
56
|
-
if (filterFunction) {
|
|
57
|
-
if (filterFunction(filePath))
|
|
58
|
-
return filePath;
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
return filePath;
|
|
62
|
-
}
|
|
63
|
-
}));
|
|
64
|
-
return files.flat().filter(Boolean);
|
|
65
|
-
}
|
|
7
|
+
const planMethods_1 = require("./planMethods");
|
|
66
8
|
async function plan(input, includeTags, excludeTags) {
|
|
67
9
|
if (!fs.existsSync(input)) {
|
|
68
10
|
throw new Error(`Flow path does not exist: ${path.resolve(input)}`);
|
|
69
11
|
}
|
|
70
12
|
if (fs.lstatSync(input).isFile()) {
|
|
71
|
-
|
|
13
|
+
const directory = path.dirname(input) + '/';
|
|
14
|
+
const { config, testSteps } = (0, planMethods_1.readTestYamlFileAsJson)(input);
|
|
15
|
+
const { allFiles, errors } = (0, planMethods_1.processDependencies)({
|
|
16
|
+
config,
|
|
17
|
+
directory,
|
|
18
|
+
input,
|
|
19
|
+
testSteps,
|
|
20
|
+
});
|
|
21
|
+
if (errors.length > 0) {
|
|
22
|
+
throw new Error('The following flow files are not present in the provided directory: \n' +
|
|
23
|
+
errors.join('\n'));
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
flowsToRun: [input.split('/').pop() ?? input],
|
|
27
|
+
referencedFiles: [...new Set(allFiles)],
|
|
28
|
+
totalFlowFiles: 1,
|
|
29
|
+
};
|
|
72
30
|
}
|
|
73
|
-
let unfilteredFlowFiles = await walk(input, isFlowFile);
|
|
31
|
+
let unfilteredFlowFiles = await (0, planMethods_1.walk)(input, planMethods_1.isFlowFile);
|
|
74
32
|
if (unfilteredFlowFiles.length === 0) {
|
|
75
33
|
throw new Error(`Flow directory does not contain any Flow files: ${path.resolve(input)}`);
|
|
76
34
|
}
|
|
@@ -81,12 +39,17 @@ async function plan(input, includeTags, excludeTags) {
|
|
|
81
39
|
const relativeFilePaths = unfilteredFlowFiles.map((file) => file.replace(cleanPath, ''));
|
|
82
40
|
const topLevelFlowFiles = relativeFilePaths.filter((file) => !file.includes('/'));
|
|
83
41
|
const workspaceConfig = configFilePath
|
|
84
|
-
? readYamlFileAsJson(configFilePath)
|
|
42
|
+
? (0, planMethods_1.readYamlFileAsJson)(configFilePath)
|
|
85
43
|
: {};
|
|
86
44
|
let unsortedFlowFiles = topLevelFlowFiles;
|
|
87
45
|
if (workspaceConfig.flows) {
|
|
88
|
-
const
|
|
89
|
-
|
|
46
|
+
const globs = workspaceConfig.flows.map((glob) => glob);
|
|
47
|
+
const matchedFiles = await (0, glob_1.glob)(globs, {
|
|
48
|
+
cwd: input,
|
|
49
|
+
nodir: true,
|
|
50
|
+
});
|
|
51
|
+
unsortedFlowFiles = matchedFiles.filter((file) => file !== 'config.yaml' &&
|
|
52
|
+
(file.endsWith('.yaml') || file.endsWith('.yml')));
|
|
90
53
|
}
|
|
91
54
|
if (unsortedFlowFiles.length === 0) {
|
|
92
55
|
const error = workspaceConfig.flows
|
|
@@ -95,10 +58,39 @@ async function plan(input, includeTags, excludeTags) {
|
|
|
95
58
|
throw error;
|
|
96
59
|
}
|
|
97
60
|
// eslint-disable-next-line unicorn/no-array-reduce
|
|
98
|
-
const configPerFlowFile = unsortedFlowFiles.reduce((acc, filePath) => {
|
|
99
|
-
|
|
61
|
+
const { configPerFlowFile } = unsortedFlowFiles.reduce((acc, filePath) => {
|
|
62
|
+
const { config } = (0, planMethods_1.readTestYamlFileAsJson)(cleanPath + filePath);
|
|
63
|
+
acc.configPerFlowFile[filePath] = config;
|
|
100
64
|
return acc;
|
|
101
|
-
}, {
|
|
65
|
+
}, {
|
|
66
|
+
configPerFlowFile: {},
|
|
67
|
+
});
|
|
68
|
+
// eslint-disable-next-line unicorn/no-array-reduce
|
|
69
|
+
const { testStepsPerFlowFile } = unfilteredFlowFiles.reduce((acc, filePath) => {
|
|
70
|
+
const { testSteps } = (0, planMethods_1.readTestYamlFileAsJson)(filePath);
|
|
71
|
+
acc.testStepsPerFlowFile[filePath] = testSteps;
|
|
72
|
+
return acc;
|
|
73
|
+
}, {
|
|
74
|
+
testStepsPerFlowFile: {},
|
|
75
|
+
});
|
|
76
|
+
let errors = [];
|
|
77
|
+
let allFiles = [];
|
|
78
|
+
for (const [filePath, testSteps] of Object.entries(testStepsPerFlowFile)) {
|
|
79
|
+
if (!testSteps)
|
|
80
|
+
break;
|
|
81
|
+
const { allFiles: deps, errors: errs } = (0, planMethods_1.processDependencies)({
|
|
82
|
+
config: configPerFlowFile[path.relative(cleanPath, filePath)],
|
|
83
|
+
directory: cleanPath,
|
|
84
|
+
input: filePath,
|
|
85
|
+
testSteps,
|
|
86
|
+
});
|
|
87
|
+
allFiles = [...allFiles, ...deps];
|
|
88
|
+
errors = [...errors, ...errs];
|
|
89
|
+
}
|
|
90
|
+
if (errors.length > 0) {
|
|
91
|
+
throw new Error('The following flow files are not present in the provided directory: \n' +
|
|
92
|
+
errors.join('\n'));
|
|
93
|
+
}
|
|
102
94
|
const allIncludeTags = [
|
|
103
95
|
...includeTags,
|
|
104
96
|
...(workspaceConfig.includeTags || []),
|
|
@@ -125,24 +117,14 @@ async function plan(input, includeTags, excludeTags) {
|
|
|
125
117
|
return acc;
|
|
126
118
|
}, {});
|
|
127
119
|
const flowsToRunInSequence = workspaceConfig.executionOrder?.flowsOrder
|
|
128
|
-
?.map((flowOrder) => getFlowsToRunInSequence(pathsByName, [flowOrder]))
|
|
120
|
+
?.map((flowOrder) => (0, planMethods_1.getFlowsToRunInSequence)(pathsByName, [flowOrder]))
|
|
129
121
|
.flat() || [];
|
|
130
122
|
const normalFlows = allFlows.filter((flow) => !flowsToRunInSequence.includes(flow));
|
|
131
|
-
// for (const filePath of allFlows) {
|
|
132
|
-
// const commands = YamlCommandReader.readCommands(filePath).filter(
|
|
133
|
-
// (command) => command.addMediaCommand,
|
|
134
|
-
// );
|
|
135
|
-
// const mediaPaths = commands.flatMap(
|
|
136
|
-
// (command) => command.addMediaCommand.mediaPaths,
|
|
137
|
-
// );
|
|
138
|
-
// YamlCommandsPathValidator.validatePathsExistInWorkspace(
|
|
139
|
-
// input,
|
|
140
|
-
// filePath,
|
|
141
|
-
// mediaPaths,
|
|
142
|
-
// );
|
|
143
|
-
// }
|
|
144
123
|
return {
|
|
124
|
+
allExcludeTags,
|
|
125
|
+
allIncludeTags,
|
|
145
126
|
flowsToRun: normalFlows,
|
|
127
|
+
referencedFiles: [...new Set(allFiles)],
|
|
146
128
|
sequence: {
|
|
147
129
|
continueOnFailure: workspaceConfig.executionOrder?.continueOnFailure,
|
|
148
130
|
flows: flowsToRunInSequence,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export declare function getFlowsToRunInSequence(paths: {
|
|
2
|
+
[key: string]: string;
|
|
3
|
+
}, flowOrder: string[]): string[];
|
|
4
|
+
export declare function isFlowFile(filePath: string): boolean;
|
|
5
|
+
export declare const readYamlFileAsJson: (filePath: string) => unknown;
|
|
6
|
+
export declare const readTestYamlFileAsJson: (filePath: string) => {
|
|
7
|
+
config: Record<string, unknown>;
|
|
8
|
+
testSteps: Record<string, unknown>[];
|
|
9
|
+
} | {
|
|
10
|
+
config: null;
|
|
11
|
+
testSteps: Record<string, unknown>[];
|
|
12
|
+
};
|
|
13
|
+
export declare function walk(dir: string, filterFunction?: (filePath: string) => boolean): Promise<string[]>;
|
|
14
|
+
export declare const checkIfFilesExistInWorkspace: (commandName: string, command: Record<string, string> | string | string[], filePath: string, cleanPath: string) => {
|
|
15
|
+
errors: string[];
|
|
16
|
+
files: string[];
|
|
17
|
+
};
|
|
18
|
+
interface IProcessDependencies {
|
|
19
|
+
config?: Record<string, unknown> | null;
|
|
20
|
+
directory: string;
|
|
21
|
+
input: string;
|
|
22
|
+
testSteps: Record<string, unknown>[];
|
|
23
|
+
}
|
|
24
|
+
export declare const processDependencies: ({ config, directory, input, testSteps, }: IProcessDependencies) => {
|
|
25
|
+
allFiles: string[];
|
|
26
|
+
errors: string[];
|
|
27
|
+
};
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.processDependencies = exports.checkIfFilesExistInWorkspace = exports.walk = exports.readTestYamlFileAsJson = exports.readYamlFileAsJson = exports.isFlowFile = exports.getFlowsToRunInSequence = void 0;
|
|
4
|
+
/* eslint-disable unicorn/filename-case */
|
|
5
|
+
const yaml = require("js-yaml");
|
|
6
|
+
const fs = require("node:fs");
|
|
7
|
+
const path = require("node:path");
|
|
8
|
+
const commandsThatRequireFiles = new Set(['addMedia', 'runFlow', 'runScript']);
|
|
9
|
+
function getFlowsToRunInSequence(paths, flowOrder) {
|
|
10
|
+
if (flowOrder.length === 0)
|
|
11
|
+
return [];
|
|
12
|
+
const orderSet = new Set(flowOrder);
|
|
13
|
+
const namesInOrder = Object.keys(paths).filter((key) => orderSet.has(key));
|
|
14
|
+
if (namesInOrder.length === 0)
|
|
15
|
+
return [];
|
|
16
|
+
const result = [...orderSet].filter((item) => namesInOrder.includes(item));
|
|
17
|
+
if (result.length === 0) {
|
|
18
|
+
throw new Error(`Could not find flows needed for execution in order: ${[...orderSet]
|
|
19
|
+
.filter((item) => !namesInOrder.includes(item))
|
|
20
|
+
.join(', ')}`);
|
|
21
|
+
}
|
|
22
|
+
else if (flowOrder
|
|
23
|
+
.slice(0, result.length)
|
|
24
|
+
.every((value, index) => value === result[index])) {
|
|
25
|
+
return result.map((item) => paths[item]);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
exports.getFlowsToRunInSequence = getFlowsToRunInSequence;
|
|
32
|
+
function isFlowFile(filePath) {
|
|
33
|
+
return filePath.endsWith('.yaml') || filePath.endsWith('.yml');
|
|
34
|
+
}
|
|
35
|
+
exports.isFlowFile = isFlowFile;
|
|
36
|
+
const readYamlFileAsJson = (filePath) => {
|
|
37
|
+
const yamlText = fs.readFileSync(filePath, 'utf8');
|
|
38
|
+
return yaml.load(yamlText);
|
|
39
|
+
};
|
|
40
|
+
exports.readYamlFileAsJson = readYamlFileAsJson;
|
|
41
|
+
const readTestYamlFileAsJson = (filePath) => {
|
|
42
|
+
const yamlText = fs.readFileSync(filePath, 'utf8');
|
|
43
|
+
if (yamlText.includes('\n---\n')) {
|
|
44
|
+
const yamlTexts = yamlText.split('\n---\n');
|
|
45
|
+
const config = yaml.load(yamlTexts[0]);
|
|
46
|
+
const testSteps = yaml.load(yamlTexts[1]);
|
|
47
|
+
if (Object.keys(config ?? {}).length > 0) {
|
|
48
|
+
return { config, testSteps };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const testSteps = yaml.load(yamlText);
|
|
52
|
+
if (Object.keys(testSteps).length > 0) {
|
|
53
|
+
return { config: null, testSteps };
|
|
54
|
+
}
|
|
55
|
+
return { config: null, testSteps };
|
|
56
|
+
};
|
|
57
|
+
exports.readTestYamlFileAsJson = readTestYamlFileAsJson;
|
|
58
|
+
async function walk(dir, filterFunction) {
|
|
59
|
+
const readDirResult = await fs.promises.readdir(dir);
|
|
60
|
+
const files = await Promise.all(readDirResult.map(async (file) => {
|
|
61
|
+
const filePath = path.join(dir, file);
|
|
62
|
+
const stats = await fs.promises.stat(filePath);
|
|
63
|
+
if (stats.isDirectory())
|
|
64
|
+
return walk(filePath, filterFunction);
|
|
65
|
+
if (stats.isFile())
|
|
66
|
+
if (filterFunction) {
|
|
67
|
+
if (filterFunction(filePath))
|
|
68
|
+
return filePath;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
return filePath;
|
|
72
|
+
}
|
|
73
|
+
}));
|
|
74
|
+
return files.flat().filter(Boolean);
|
|
75
|
+
}
|
|
76
|
+
exports.walk = walk;
|
|
77
|
+
const checkIfFilesExistInWorkspace = (commandName, command, filePath, cleanPath) => {
|
|
78
|
+
const errors = [];
|
|
79
|
+
const files = [];
|
|
80
|
+
const directory = path.dirname(filePath);
|
|
81
|
+
const buildError = (error) => `Flow file "${filePath.replace(cleanPath, './')}" has a command "${commandName}" that references a ${error} ${JSON.stringify(command)}`;
|
|
82
|
+
const processFilePath = (relativePath) => {
|
|
83
|
+
const absoluteFilePath = path.resolve(directory, relativePath);
|
|
84
|
+
const error = checkFile(absoluteFilePath, cleanPath);
|
|
85
|
+
if (error)
|
|
86
|
+
errors.push(buildError(error));
|
|
87
|
+
files.push(absoluteFilePath.replace(cleanPath, './'));
|
|
88
|
+
};
|
|
89
|
+
// simple command
|
|
90
|
+
if (typeof command === 'string')
|
|
91
|
+
processFilePath(command);
|
|
92
|
+
// array command
|
|
93
|
+
if (Array.isArray(command)) {
|
|
94
|
+
for (const file of command) {
|
|
95
|
+
processFilePath(file);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// object command
|
|
99
|
+
const x = command; // prevent annoying ts error
|
|
100
|
+
if (typeof command === 'object' && x?.file)
|
|
101
|
+
processFilePath(x.file);
|
|
102
|
+
return { errors, files };
|
|
103
|
+
};
|
|
104
|
+
exports.checkIfFilesExistInWorkspace = checkIfFilesExistInWorkspace;
|
|
105
|
+
const checkFile = (filePath, cleanPath) => {
|
|
106
|
+
if (!fs.existsSync(filePath))
|
|
107
|
+
return `non-existent file`;
|
|
108
|
+
if (!filePath.startsWith(cleanPath))
|
|
109
|
+
return `file outside the workspace`;
|
|
110
|
+
};
|
|
111
|
+
const processDependencies = ({ config, directory, input, testSteps, }) => {
|
|
112
|
+
let errors = [];
|
|
113
|
+
let allFiles = [];
|
|
114
|
+
const { onFlowComplete, onFlowStart } = config ?? {};
|
|
115
|
+
const stepsArray = [testSteps];
|
|
116
|
+
if (onFlowStart)
|
|
117
|
+
stepsArray.push(onFlowStart);
|
|
118
|
+
if (onFlowComplete)
|
|
119
|
+
stepsArray.push(onFlowComplete);
|
|
120
|
+
for (const steps of stepsArray) {
|
|
121
|
+
for (const command of steps) {
|
|
122
|
+
if (typeof command === 'string')
|
|
123
|
+
continue;
|
|
124
|
+
for (const [commandName, commandValue] of Object.entries(command)) {
|
|
125
|
+
if (commandsThatRequireFiles.has(commandName)) {
|
|
126
|
+
const { errors: newErrors, files } = (0, exports.checkIfFilesExistInWorkspace)(commandName, commandValue, path.normalize(input), directory);
|
|
127
|
+
errors = [...errors, ...newErrors];
|
|
128
|
+
allFiles = [...allFiles, ...files];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { allFiles, errors };
|
|
134
|
+
};
|
|
135
|
+
exports.processDependencies = processDependencies;
|
package/oclif.manifest.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"aliases": [],
|
|
5
5
|
"args": {
|
|
6
6
|
"firstFile": {
|
|
7
|
-
"description": "The binary file of the app to run your flow against, e.g.
|
|
7
|
+
"description": "The binary file of the app to run your flow against, e.g. test.apk for android or test.app/.zip for ios",
|
|
8
8
|
"hidden": true,
|
|
9
9
|
"name": "firstFile"
|
|
10
10
|
},
|
|
@@ -220,5 +220,5 @@
|
|
|
220
220
|
]
|
|
221
221
|
}
|
|
222
222
|
},
|
|
223
|
-
"version": "0.0.
|
|
223
|
+
"version": "0.0.5"
|
|
224
224
|
}
|
package/package.json
CHANGED
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
"@oclif/plugin-update": "^4.1.11",
|
|
13
13
|
"@oclif/plugin-warn-if-update-available": "^3.0.10",
|
|
14
14
|
"archiver": "^6.0.1",
|
|
15
|
-
"glob
|
|
16
|
-
"js-yaml": "^4.1.0"
|
|
15
|
+
"glob": "^10.3.10",
|
|
16
|
+
"js-yaml": "^4.1.0",
|
|
17
|
+
"node-stream-zip": "^1.15.0"
|
|
17
18
|
},
|
|
18
19
|
"description": "Better cloud maestro testing",
|
|
19
20
|
"devDependencies": {
|
|
@@ -72,7 +73,7 @@
|
|
|
72
73
|
"test": "mocha --forbid-only \"test/**/*.test.ts\"",
|
|
73
74
|
"version": "oclif readme && git add README.md"
|
|
74
75
|
},
|
|
75
|
-
"version": "0.0.
|
|
76
|
+
"version": "0.0.5",
|
|
76
77
|
"bugs": {
|
|
77
78
|
"url": "https://discord.gg/gm3mJwcNw8"
|
|
78
79
|
},
|