@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.
@@ -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>>;
@@ -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 node_stream_1 = require("node:stream");
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. app.apk for android or app.zip for ios',
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
- let testFileNames = [];
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
- const executionPlan = await (0, plan_1.plan)(flowFile, includeTags.flat(), excludeTags.flat());
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 (!finalAppFile.endsWith('.apk') && !finalAppFile.endsWith('.zip')) {
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
- Submitting new job
301
- → Flow(s): ${flowFile}
302
- → App: ${appBinaryId || finalAppFile}
116
+ Submitting new job
117
+ → Flow(s): ${flowFile}
118
+ → App: ${appBinaryId || finalAppFile}
303
119
 
304
- With options
305
- → ${flagLogs.join(`
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
- const binaryBlob = new Blob([await (0, promises_1.readFile)(finalAppFile)], {
313
- type: mimeTypeLookupByExtension[finalAppFile.split('.').pop()],
314
- });
315
- binaryFormData.set('file', binaryBlob, finalAppFile);
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 path = flowFile.split('/').slice(0, -1).join('/');
335
- const buffer = await compressDir(path?.length ? path : './');
336
- // const buffer =
337
- // totalFlowFiles > 1 || flowFile?.endsWith('/')
338
- // ? await compressDir(
339
- // flowFile!.split('/').slice(0, -1).join('/') ?? '.',
340
- // )
341
- // : await compressFile(flowFile!);
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({ continueOnFailure, orientation }));
352
- if (androidApiLevel) {
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>;
@@ -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
@@ -1,5 +1,8 @@
1
1
  interface IExecutionPlan {
2
+ allExcludeTags?: null | string[];
3
+ allIncludeTags?: null | string[];
2
4
  flowsToRun: string[];
5
+ referencedFiles: string[];
3
6
  sequence?: IFlowSequence | null;
4
7
  totalFlowFiles: number;
5
8
  }
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 globToRegExp = require("glob-to-regexp");
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
- function getFlowsToRunInSequence(paths, flowOrder) {
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
- return { flowsToRun: [input.split('/').pop() ?? input], totalFlowFiles: 1 };
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 matchers = workspaceConfig.flows.map((glob) => glob === '*' ? /^((?!\/).)*$/ : globToRegExp(glob));
89
- unsortedFlowFiles = relativeFilePaths.filter((filePath) => matchers.some((matcher) => matcher.test(filePath)));
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
- acc[filePath] = readConfigFromYamlFileAsJson(cleanPath + filePath);
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;
@@ -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. app.apk for android or app.zip for ios",
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.3"
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-to-regexp": "^0.4.1",
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.3",
76
+ "version": "0.0.5",
76
77
  "bugs": {
77
78
  "url": "https://discord.gg/gm3mJwcNw8"
78
79
  },