@devicecloud.dev/dcd 4.1.2-beta.1 → 4.1.3

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.
Files changed (50) hide show
  1. package/dist/commands/cloud.d.ts +26 -34
  2. package/dist/commands/cloud.js +117 -465
  3. package/dist/commands/status.d.ts +6 -0
  4. package/dist/commands/status.js +19 -1
  5. package/dist/config/flags/api.flags.d.ts +7 -0
  6. package/dist/config/flags/api.flags.js +19 -0
  7. package/dist/config/flags/binary.flags.d.ts +8 -0
  8. package/dist/config/flags/binary.flags.js +20 -0
  9. package/dist/config/flags/device.flags.d.ts +14 -0
  10. package/dist/config/flags/device.flags.js +46 -0
  11. package/dist/config/flags/environment.flags.d.ts +11 -0
  12. package/dist/config/flags/environment.flags.js +37 -0
  13. package/dist/config/flags/execution.flags.d.ts +13 -0
  14. package/dist/config/flags/execution.flags.js +50 -0
  15. package/dist/config/flags/output.flags.d.ts +18 -0
  16. package/dist/config/flags/output.flags.js +61 -0
  17. package/dist/constants.d.ts +28 -24
  18. package/dist/constants.js +21 -206
  19. package/dist/gateways/api-gateway.d.ts +3 -3
  20. package/dist/methods.d.ts +0 -4
  21. package/dist/methods.js +15 -80
  22. package/dist/services/device-validation.service.d.ts +29 -0
  23. package/dist/services/device-validation.service.js +72 -0
  24. package/dist/{plan.d.ts → services/execution-plan.service.d.ts} +1 -1
  25. package/dist/{plan.js → services/execution-plan.service.js} +10 -10
  26. package/dist/{planMethods.js → services/execution-plan.utils.js} +0 -1
  27. package/dist/services/metadata-extractor.service.d.ts +46 -0
  28. package/dist/services/metadata-extractor.service.js +138 -0
  29. package/dist/services/moropo.service.d.ts +20 -0
  30. package/dist/services/moropo.service.js +113 -0
  31. package/dist/services/report-download.service.d.ts +40 -0
  32. package/dist/services/report-download.service.js +110 -0
  33. package/dist/services/results-polling.service.d.ts +45 -0
  34. package/dist/services/results-polling.service.js +210 -0
  35. package/dist/services/test-submission.service.d.ts +41 -0
  36. package/dist/services/test-submission.service.js +116 -0
  37. package/dist/services/version.service.d.ts +31 -0
  38. package/dist/services/version.service.js +81 -0
  39. package/dist/types/{schema.types.d.ts → generated/schema.types.d.ts} +349 -349
  40. package/dist/types/index.d.ts +6 -0
  41. package/dist/types/index.js +24 -0
  42. package/dist/utils/compatibility.d.ts +5 -0
  43. package/dist/utils/connectivity.d.ts +29 -0
  44. package/dist/utils/connectivity.js +100 -0
  45. package/oclif.manifest.json +195 -209
  46. package/package.json +2 -9
  47. /package/dist/{planMethods.d.ts → services/execution-plan.utils.d.ts} +0 -0
  48. /package/dist/types/{device.types.d.ts → domain/device.types.d.ts} +0 -0
  49. /package/dist/types/{device.types.js → domain/device.types.js} +0 -0
  50. /package/dist/types/{schema.types.js → generated/schema.types.js} +0 -0
package/dist/constants.js CHANGED
@@ -1,210 +1,25 @@
1
1
  "use strict";
2
+ /**
3
+ * Consolidated flag definitions for backward compatibility.
4
+ * Flags are organized by domain in src/config/flags/
5
+ */
2
6
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.flags = exports.resolveMaestroVersion = exports.getLatestMaestroVersion = exports.DEFAULT_MAESTRO_VERSION = exports.SUPPORTED_MAESTRO_VERSIONS = void 0;
4
- const core_1 = require("@oclif/core");
5
- const device_types_1 = require("./types/device.types");
6
- // Centralized maestro version definitions - single source of truth
7
- exports.SUPPORTED_MAESTRO_VERSIONS = [
8
- '1.39.0',
9
- '1.39.1',
10
- '1.39.2',
11
- '1.39.5',
12
- '1.39.7',
13
- '1.40.3',
14
- '1.41.0',
15
- '2.0.2',
16
- '2.0.3',
17
- '2.0.4',
18
- ];
19
- exports.DEFAULT_MAESTRO_VERSION = '1.41.0';
20
- const getLatestMaestroVersion = () => exports.SUPPORTED_MAESTRO_VERSIONS.at(-1);
21
- exports.getLatestMaestroVersion = getLatestMaestroVersion;
22
- const resolveMaestroVersion = (version) => {
23
- if (version === 'latest') {
24
- return (0, exports.getLatestMaestroVersion)();
25
- }
26
- return version || exports.DEFAULT_MAESTRO_VERSION;
27
- };
28
- exports.resolveMaestroVersion = resolveMaestroVersion;
7
+ exports.flags = void 0;
8
+ const api_flags_1 = require("./config/flags/api.flags");
9
+ const binary_flags_1 = require("./config/flags/binary.flags");
10
+ const device_flags_1 = require("./config/flags/device.flags");
11
+ const environment_flags_1 = require("./config/flags/environment.flags");
12
+ const execution_flags_1 = require("./config/flags/execution.flags");
13
+ const output_flags_1 = require("./config/flags/output.flags");
14
+ /**
15
+ * All flag definitions consolidated from domain-specific flag modules.
16
+ * Maintains backward compatibility with existing imports.
17
+ */
29
18
  exports.flags = {
30
- 'android-api-level': core_1.Flags.string({
31
- description: '[Android only] Android API level to run your flow against',
32
- options: Object.values(device_types_1.EAndroidApiLevels),
33
- }),
34
- 'android-device': core_1.Flags.string({
35
- description: '[Android only] Android device to run your flow against',
36
- options: Object.values(device_types_1.EAndroidDevices),
37
- }),
38
- apiKey: core_1.Flags.string({
39
- aliases: ['api-key'],
40
- description: 'API key for devicecloud.dev (find this in the console UI). You can also set the DEVICE_CLOUD_API_KEY environment variable.',
41
- }),
42
- apiUrl: core_1.Flags.string({
43
- aliases: ['api-url', 'apiURL'],
44
- default: 'https://api.devicecloud.dev',
45
- description: 'API base URL',
46
- hidden: true,
47
- }),
48
- 'app-binary-id': core_1.Flags.string({
49
- aliases: ['app-binary-id'],
50
- description: 'The ID of the app binary previously uploaded to devicecloud.dev',
51
- }),
52
- 'app-file': core_1.Flags.file({
53
- aliases: ['app-file'],
54
- description: 'App binary to run your flows against',
55
- }),
56
- 'artifacts-path': core_1.Flags.string({
57
- dependsOn: ['download-artifacts'],
58
- description: 'Custom file path for downloaded artifacts (default: ./artifacts.zip)',
59
- }),
60
- 'junit-path': core_1.Flags.string({
61
- dependsOn: ['report'],
62
- description: 'Custom file path for downloaded JUnit report (default: ./report.xml)',
63
- }),
64
- 'allure-path': core_1.Flags.string({
65
- dependsOn: ['report'],
66
- description: 'Custom file path for downloaded Allure report (default: ./report.html)',
67
- }),
68
- 'html-path': core_1.Flags.string({
69
- dependsOn: ['report'],
70
- description: 'Custom file path for downloaded HTML report (default: ./report.html)',
71
- }),
72
- async: core_1.Flags.boolean({
73
- description: 'Immediately return (exit code 0) from the command without waiting for the results of the run (useful for saving CI minutes)',
74
- }),
75
- config: core_1.Flags.file({
76
- description: 'Path to custom config.yaml file. If not provided, defaults to config.yaml in root flows folders.',
77
- }),
78
- debug: core_1.Flags.boolean({
79
- default: false,
80
- description: 'Enable detailed debug logging for troubleshooting issues',
81
- }),
82
- 'device-locale': core_1.Flags.string({
83
- description: 'Locale that will be set to a device, ISO-639-1 code and uppercase ISO-3166-1 code e.g. "de_DE" for Germany',
84
- }),
85
- 'download-artifacts': core_1.Flags.string({
86
- description: 'Download a zip containing the logs, screenshots and videos for each result in this run. You will be debited a $0.01 egress fee for each result. Options: ALL (everything), FAILED (failures only).',
87
- options: ['ALL', 'FAILED'],
88
- }),
89
- 'dry-run': core_1.Flags.boolean({
90
- default: false,
91
- description: 'Simulate the run without actually triggering the upload/test, useful for debugging workflow issues.',
92
- }),
93
- env: core_1.Flags.file({
94
- char: 'e',
95
- description: 'One or more environment variables to inject into your flows',
96
- multiple: true,
97
- multipleNonGreedy: true,
98
- }),
99
- 'exclude-flows': core_1.Flags.string({
100
- default: [],
101
- description: 'Sub directories to ignore when building the flow file list',
102
- multiple: true,
103
- multipleNonGreedy: true,
104
- parse: (input) => input.split(','),
105
- }),
106
- 'exclude-tags': core_1.Flags.string({
107
- aliases: ['exclude-tags'],
108
- default: [],
109
- description: 'Flows which have these tags will be excluded from the run',
110
- multiple: true,
111
- multipleNonGreedy: true,
112
- parse: (input) => input.split(','),
113
- }),
114
- flows: core_1.Flags.string({
115
- description: 'The path to the flow file or folder containing your flows',
116
- }),
117
- 'google-play': core_1.Flags.boolean({
118
- aliases: ['google-play'],
119
- default: false,
120
- description: '[Android only] Run your flow against Google Play devices',
121
- }),
122
- 'ignore-sha-check': core_1.Flags.boolean({
123
- description: 'Ignore the sha hash check and upload the binary regardless of whether it already exists (not recommended)',
124
- }),
125
- 'include-tags': core_1.Flags.string({
126
- aliases: ['include-tags'],
127
- default: [],
128
- description: 'Only flows which have these tags will be included in the run',
129
- multiple: true,
130
- multipleNonGreedy: true,
131
- parse: (input) => input.split(','),
132
- }),
133
- 'ios-device': core_1.Flags.string({
134
- description: '[iOS only] iOS device to run your flow against',
135
- options: Object.values(device_types_1.EiOSDevices),
136
- }),
137
- 'ios-version': core_1.Flags.string({
138
- description: '[iOS only] iOS version to run your flow against',
139
- options: Object.values(device_types_1.EiOSVersions),
140
- }),
141
- json: core_1.Flags.boolean({
142
- description: 'Output results in JSON format - note: will always provide exit code 0',
143
- }),
144
- 'json-file': core_1.Flags.boolean({
145
- description: 'Write JSON output to a file. File be called <upload_id>_dcd.json unless you supply the --json-file-name flag - note: will always exit with code 0',
146
- required: false,
147
- }),
148
- 'json-file-name': core_1.Flags.string({
149
- description: 'A custom name for the JSON file (can also include relative path)',
150
- dependsOn: ['json-file'],
151
- }),
152
- 'maestro-version': core_1.Flags.string({
153
- aliases: ['maestroVersion'],
154
- default: exports.DEFAULT_MAESTRO_VERSION,
155
- description: 'Maestro version to run your flow against',
156
- options: [...exports.SUPPORTED_MAESTRO_VERSIONS, 'latest'],
157
- }),
158
- metadata: core_1.Flags.string({
159
- char: 'm',
160
- description: 'Arbitrary key-value metadata to include with your test run (format: key=value)',
161
- multiple: true,
162
- multipleNonGreedy: true,
163
- }),
164
- mitmHost: core_1.Flags.string({
165
- description: 'used for mitmproxy support, enterprise only, contact support if interested',
166
- hidden: true,
167
- }),
168
- mitmPath: core_1.Flags.string({
169
- dependsOn: ['mitmHost'],
170
- description: 'used for mitmproxy support, enterprise only, contact support if interested',
171
- hidden: true,
172
- }),
173
- 'moropo-v1-api-key': core_1.Flags.string({
174
- description: 'API key for Moropo v1 integration',
175
- required: false,
176
- }),
177
- name: core_1.Flags.string({
178
- description: 'A custom name for your upload (useful for tagging commits etc)',
179
- }),
180
- orientation: core_1.Flags.string({
181
- description: '[Android only] The orientation of the device to run your flow against in degrees',
182
- options: ['0', '90'],
183
- }),
184
- quiet: core_1.Flags.boolean({
185
- char: 'q',
186
- default: false,
187
- description: 'Quieter console output that wont provide progress updates',
188
- }),
189
- report: core_1.Flags.string({
190
- aliases: ['format'],
191
- description: 'Generate and download test reports in the specified format. Use "allure" for a complete HTML report.',
192
- options: ['allure', 'junit', 'html'],
193
- }),
194
- retry: core_1.Flags.integer({
195
- description: 'Automatically retry the run up to the number of times specified (same as pressing retry in the UI) - this is free of charge',
196
- }),
197
- 'runner-type': core_1.Flags.string({
198
- default: 'default',
199
- description: '[experimental] The type of runner to use - note: anything other than default will incur premium pricing tiers, see https://docs.devicecloud.dev/reference/runner-type for more information. gpu1 is Android-only and requires contacting support to enable, otherwise reverts to default.',
200
- options: ['default', 'm4', 'm1', 'gpu1'],
201
- }),
202
- 'show-crosshairs': core_1.Flags.boolean({
203
- default: false,
204
- description: '[Android only] Display crosshairs for screen interactions during test execution',
205
- }),
206
- 'skip-chrome-onboarding': core_1.Flags.boolean({
207
- default: false,
208
- description: '[Android only] Skip Chrome browser onboarding screens when running tests',
209
- }),
19
+ ...api_flags_1.apiFlags,
20
+ ...binary_flags_1.binaryFlags,
21
+ ...device_flags_1.deviceFlags,
22
+ ...environment_flags_1.environmentFlags,
23
+ ...execution_flags_1.executionFlags,
24
+ ...output_flags_1.outputFlags,
210
25
  };
@@ -14,14 +14,14 @@ export declare const ApiGateway: {
14
14
  downloadArtifactsZip(baseUrl: string, apiKey: string, uploadId: string, results: "ALL" | "FAILED", artifactsPath?: string): Promise<void>;
15
15
  finaliseUpload(baseUrl: string, apiKey: string, id: string, metadata: TAppMetadata, path: string, sha: string): Promise<Record<string, never>>;
16
16
  getBinaryUploadUrl(baseUrl: string, apiKey: string, platform: "android" | "ios"): Promise<{
17
+ id: string;
17
18
  message: string;
18
19
  path: string;
19
20
  token: string;
20
- id: string;
21
21
  }>;
22
22
  getResultsForUpload(baseUrl: string, apiKey: string, uploadId: string): Promise<{
23
+ results?: import("../types/generated/schema.types").components["schemas"]["TResultResponse"][];
23
24
  statusCode?: number;
24
- results?: import("../types/schema.types").components["schemas"]["TResultResponse"][];
25
25
  }>;
26
26
  getUploadStatus(baseUrl: string, apiKey: string, options: {
27
27
  name?: string;
@@ -37,7 +37,7 @@ export declare const ApiGateway: {
37
37
  }>;
38
38
  uploadFlow(baseUrl: string, apiKey: string, testFormData: FormData): Promise<{
39
39
  message?: string;
40
- results?: import("../types/schema.types").components["schemas"]["IDBResult"][];
40
+ results?: import("../types/generated/schema.types").components["schemas"]["IDBResult"][];
41
41
  }>;
42
42
  /**
43
43
  * Generic report download method that handles both junit and allure reports
package/dist/methods.d.ts CHANGED
@@ -1,12 +1,8 @@
1
1
  import * as archiver from 'archiver';
2
- import { TAppMetadata } from './types';
3
2
  export declare const toBuffer: (archive: archiver.Archiver) => Promise<Buffer<ArrayBuffer>>;
4
3
  export declare const compressFolderToBlob: (sourceDir: string) => Promise<Blob>;
5
4
  export declare const compressFilesFromRelativePath: (basePath: string, files: string[], commonRoot: string) => Promise<Buffer<ArrayBuffer>>;
6
5
  export declare const verifyAppZip: (zipPath: string) => Promise<void>;
7
- export declare const extractAppMetadataAndroid: (appFilePath: string) => Promise<TAppMetadata>;
8
- export declare const extractAppMetadataIosZip: (appFilePath: string) => Promise<TAppMetadata>;
9
- export declare const extractAppMetadataIos: (appFolderPath: string) => Promise<TAppMetadata>;
10
6
  export declare const uploadBinary: (filePath: string, apiUrl: string, apiKey: string, ignoreShaCheck?: boolean, log?: boolean) => Promise<string>;
11
7
  /**
12
8
  * Writes JSON data to a file with error handling
package/dist/methods.js CHANGED
@@ -1,22 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.formatDurationSeconds = exports.writeJSONFile = exports.uploadBinary = exports.extractAppMetadataIos = exports.extractAppMetadataIosZip = exports.extractAppMetadataAndroid = exports.verifyAppZip = exports.compressFilesFromRelativePath = exports.compressFolderToBlob = exports.toBuffer = void 0;
3
+ exports.formatDurationSeconds = exports.writeJSONFile = exports.uploadBinary = exports.verifyAppZip = exports.compressFilesFromRelativePath = exports.compressFolderToBlob = exports.toBuffer = void 0;
4
4
  const core_1 = require("@oclif/core");
5
- // required polyfill for node 18
6
- const file_1 = require("@web-std/file");
7
- const AppInfoParser = require("app-info-parser");
8
5
  const archiver = require("archiver");
9
- const bplist_parser_1 = require("bplist-parser");
10
6
  const node_crypto_1 = require("node:crypto");
11
7
  const node_fs_1 = require("node:fs");
12
8
  const promises_1 = require("node:fs/promises");
13
9
  const path = require("node:path");
14
10
  const node_stream_1 = require("node:stream");
15
11
  const StreamZip = require("node-stream-zip");
16
- const plist_1 = require("plist");
17
- const cloud_1 = require("./commands/cloud");
18
12
  const api_gateway_1 = require("./gateways/api-gateway");
19
13
  const supabase_gateway_1 = require("./gateways/supabase-gateway");
14
+ const metadata_extractor_service_1 = require("./services/metadata-extractor.service");
15
+ const mimeTypeLookupByExtension = {
16
+ apk: 'application/vnd.android.package-archive',
17
+ yaml: 'application/x-yaml',
18
+ zip: 'application/zip',
19
+ };
20
20
  const toBuffer = async (archive) => {
21
21
  const chunks = [];
22
22
  const writable = new node_stream_1.Writable();
@@ -76,64 +76,6 @@ const verifyAppZip = async (zipPath) => {
76
76
  zip.close();
77
77
  };
78
78
  exports.verifyAppZip = verifyAppZip;
79
- const extractAppMetadataAndroid = async (appFilePath) => {
80
- const parser = new AppInfoParser(appFilePath);
81
- const result = await parser.parse();
82
- return { appId: result.package, platform: 'android' };
83
- };
84
- exports.extractAppMetadataAndroid = extractAppMetadataAndroid;
85
- const parseInfoPlist = async (buffer) => {
86
- let data;
87
- const bufferType = buffer[0];
88
- if (bufferType === 60 ||
89
- bufferType === '<' ||
90
- bufferType === 239) {
91
- data = (0, plist_1.parse)(buffer.toString());
92
- }
93
- else if (bufferType === 98) {
94
- data = (0, bplist_parser_1.parseBuffer)(buffer)[0];
95
- }
96
- else {
97
- throw new Error('Unknown plist buffer type.');
98
- }
99
- return data;
100
- };
101
- const extractAppMetadataIosZip = async (appFilePath) => new Promise((resolve, reject) => {
102
- const zip = new StreamZip({ file: appFilePath });
103
- zip.on('ready', () => {
104
- // Get all entries and sort them by path depth
105
- const entries = Object.values(zip.entries());
106
- const sortedEntries = entries.sort((a, b) => {
107
- const aDepth = a.name.split('/').length;
108
- const bDepth = b.name.split('/').length;
109
- return aDepth - bDepth;
110
- });
111
- // Find the first Info.plist in the shallowest directory
112
- const infoPlist = sortedEntries.find((e) => e.name.endsWith('.app/Info.plist'));
113
- if (!infoPlist) {
114
- reject(new Error('Failed to find info plist'));
115
- return;
116
- }
117
- const buffer = zip.entryDataSync(infoPlist.name);
118
- parseInfoPlist(buffer)
119
- .then((data) => {
120
- const appId = data.CFBundleIdentifier;
121
- zip.close();
122
- resolve({ appId, platform: 'ios' });
123
- })
124
- .catch(reject);
125
- });
126
- zip.on('error', reject);
127
- });
128
- exports.extractAppMetadataIosZip = extractAppMetadataIosZip;
129
- const extractAppMetadataIos = async (appFolderPath) => {
130
- const infoPlistPath = path.normalize(path.join(appFolderPath, 'Info.plist'));
131
- const buffer = await (0, promises_1.readFile)(infoPlistPath);
132
- const data = await parseInfoPlist(buffer);
133
- const appId = data.CFBundleIdentifier;
134
- return { appId, platform: 'ios' };
135
- };
136
- exports.extractAppMetadataIos = extractAppMetadataIos;
137
79
  const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false, log = true) => {
138
80
  if (log) {
139
81
  core_1.ux.action.start('Checking and uploading binary', 'Initializing', {
@@ -143,14 +85,14 @@ const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false, lo
143
85
  let file;
144
86
  if (filePath?.endsWith('.app')) {
145
87
  const zippedAppBlob = await (0, exports.compressFolderToBlob)(filePath);
146
- file = new file_1.File([zippedAppBlob], filePath + '.zip');
88
+ file = new File([zippedAppBlob], filePath + '.zip');
147
89
  }
148
90
  else {
149
91
  const fileBuffer = await (0, promises_1.readFile)(filePath);
150
92
  const binaryBlob = new Blob([new Uint8Array(fileBuffer)], {
151
- type: cloud_1.mimeTypeLookupByExtension[filePath.split('.').pop()],
93
+ type: mimeTypeLookupByExtension[filePath.split('.').pop()],
152
94
  });
153
- file = new file_1.File([binaryBlob], filePath);
95
+ file = new File([binaryBlob], filePath);
154
96
  }
155
97
  let sha;
156
98
  try {
@@ -179,18 +121,11 @@ const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false, lo
179
121
  const { id, message, path, token } = await api_gateway_1.ApiGateway.getBinaryUploadUrl(apiUrl, apiKey, filePath?.endsWith('.apk') ? 'android' : 'ios');
180
122
  if (!path)
181
123
  throw new Error(message);
182
- let metadata;
183
- try {
184
- metadata = filePath?.endsWith('.apk')
185
- ? await (0, exports.extractAppMetadataAndroid)(filePath)
186
- : filePath?.endsWith('.zip')
187
- ? await (0, exports.extractAppMetadataIosZip)(filePath)
188
- : await (0, exports.extractAppMetadataIos)(filePath);
189
- }
190
- catch {
191
- if (log) {
192
- core_1.ux.warn('Failed to extract app metadata, please share with support@devicecloud.dev so we can improve our parsing.');
193
- }
124
+ // Extract app metadata using the service
125
+ const metadataExtractor = new metadata_extractor_service_1.MetadataExtractorService();
126
+ const metadata = await metadataExtractor.extract(filePath);
127
+ if (!metadata) {
128
+ throw new Error(`Failed to extract metadata from ${filePath}. Supported formats: .apk, .app, .zip`);
194
129
  }
195
130
  const env = apiUrl === 'https://api.devicecloud.dev' ? 'prod' : 'dev';
196
131
  await supabase_gateway_1.SupabaseGateway.uploadToSignedUrl(env, path, token, file);
@@ -0,0 +1,29 @@
1
+ import { CompatibilityData } from '../utils/compatibility';
2
+ export interface DeviceValidationOptions {
3
+ debug?: boolean;
4
+ logger?: (message: string) => void;
5
+ }
6
+ /**
7
+ * Service for validating device configurations against compatibility data
8
+ */
9
+ export declare class DeviceValidationService {
10
+ /**
11
+ * Validate Android device configuration
12
+ * @param androidApiLevel Android API level to validate
13
+ * @param androidDevice Android device model to validate
14
+ * @param googlePlay Whether Google Play services are enabled
15
+ * @param compatibilityData Compatibility data from API
16
+ * @param options Validation options
17
+ * @throws Error if device/API level combination is not supported
18
+ */
19
+ validateAndroidDevice(androidApiLevel: string | undefined, androidDevice: string | undefined, googlePlay: boolean, compatibilityData: CompatibilityData, options?: DeviceValidationOptions): void;
20
+ /**
21
+ * Validate iOS device configuration
22
+ * @param iOSVersion iOS version to validate
23
+ * @param iOSDevice iOS device model to validate
24
+ * @param compatibilityData Compatibility data from API
25
+ * @param options Validation options
26
+ * @throws Error if device/version combination is not supported
27
+ */
28
+ validateiOSDevice(iOSVersion: string | undefined, iOSDevice: string | undefined, compatibilityData: CompatibilityData, options?: DeviceValidationOptions): void;
29
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DeviceValidationService = void 0;
4
+ /**
5
+ * Service for validating device configurations against compatibility data
6
+ */
7
+ class DeviceValidationService {
8
+ /**
9
+ * Validate Android device configuration
10
+ * @param androidApiLevel Android API level to validate
11
+ * @param androidDevice Android device model to validate
12
+ * @param googlePlay Whether Google Play services are enabled
13
+ * @param compatibilityData Compatibility data from API
14
+ * @param options Validation options
15
+ * @throws Error if device/API level combination is not supported
16
+ */
17
+ validateAndroidDevice(androidApiLevel, androidDevice, googlePlay, compatibilityData, options = {}) {
18
+ const { debug = false, logger } = options;
19
+ if (!androidApiLevel && !androidDevice) {
20
+ return;
21
+ }
22
+ const androidDeviceID = androidDevice || 'pixel-7';
23
+ const lookup = googlePlay
24
+ ? compatibilityData.androidPlay
25
+ : compatibilityData.android;
26
+ const supportedAndroidVersions = lookup?.[androidDeviceID] || [];
27
+ const version = androidApiLevel || '34';
28
+ if (supportedAndroidVersions.length === 0) {
29
+ throw new Error(`We don't support that device configuration - please check the docs for supported devices: https://docs.devicecloud.dev/getting-started/devices-configuration`);
30
+ }
31
+ if (Array.isArray(supportedAndroidVersions) &&
32
+ !supportedAndroidVersions.includes(version)) {
33
+ throw new Error(`${androidDeviceID} ${googlePlay ? '(Play Store) ' : ''}only supports these Android API levels: ${supportedAndroidVersions.join(', ')}`);
34
+ }
35
+ if (debug && logger) {
36
+ logger(`DEBUG: Android device: ${androidDeviceID}`);
37
+ logger(`DEBUG: Android API level: ${version}`);
38
+ logger(`DEBUG: Google Play enabled: ${googlePlay}`);
39
+ logger(`DEBUG: Supported Android versions: ${supportedAndroidVersions.join(', ')}`);
40
+ }
41
+ }
42
+ /**
43
+ * Validate iOS device configuration
44
+ * @param iOSVersion iOS version to validate
45
+ * @param iOSDevice iOS device model to validate
46
+ * @param compatibilityData Compatibility data from API
47
+ * @param options Validation options
48
+ * @throws Error if device/version combination is not supported
49
+ */
50
+ validateiOSDevice(iOSVersion, iOSDevice, compatibilityData, options = {}) {
51
+ const { debug = false, logger } = options;
52
+ if (!iOSVersion && !iOSDevice) {
53
+ return;
54
+ }
55
+ const iOSDeviceID = iOSDevice || 'iphone-14';
56
+ const supportediOSVersions = compatibilityData?.ios?.[iOSDeviceID] || [];
57
+ const version = iOSVersion || '17';
58
+ if (supportediOSVersions.length === 0) {
59
+ throw new Error(`Device ${iOSDeviceID} is not supported. Please check the docs for supported devices: https://docs.devicecloud.dev/getting-started/devices-configuration`);
60
+ }
61
+ if (Array.isArray(supportediOSVersions) &&
62
+ !supportediOSVersions.includes(version)) {
63
+ throw new Error(`${iOSDeviceID} only supports these iOS versions: ${supportediOSVersions.join(', ')}`);
64
+ }
65
+ if (debug && logger) {
66
+ logger(`DEBUG: iOS device: ${iOSDeviceID}`);
67
+ logger(`DEBUG: iOS version: ${version}`);
68
+ logger(`DEBUG: Supported iOS versions: ${supportediOSVersions.join(', ')}`);
69
+ }
70
+ }
71
+ }
72
+ exports.DeviceValidationService = DeviceValidationService;
@@ -20,7 +20,7 @@ interface IExecutionOrder {
20
20
  continueOnFailure: boolean;
21
21
  flowsOrder: string[];
22
22
  }
23
- interface IExecutionPlan {
23
+ export interface IExecutionPlan {
24
24
  allExcludeTags?: null | string[];
25
25
  allIncludeTags?: null | string[];
26
26
  flowMetadata: Record<string, Record<string, unknown>>;
@@ -4,14 +4,14 @@ exports.plan = plan;
4
4
  const glob_1 = require("glob");
5
5
  const fs = require("node:fs");
6
6
  const path = require("node:path");
7
- const planMethods_1 = require("./planMethods");
7
+ const execution_plan_utils_1 = require("./execution-plan.utils");
8
8
  async function checkDependencies(input) {
9
9
  const checkedDependencies = [];
10
10
  const uncheckedDependencies = [input];
11
11
  while (uncheckedDependencies.length > 0) {
12
12
  const fileToCheck = uncheckedDependencies.shift();
13
- const { config, testSteps } = (0, planMethods_1.readTestYamlFileAsJson)(fileToCheck);
14
- const { allErrors, allFiles } = (0, planMethods_1.processDependencies)({
13
+ const { config, testSteps } = (0, execution_plan_utils_1.readTestYamlFileAsJson)(fileToCheck);
14
+ const { allErrors, allFiles } = (0, execution_plan_utils_1.processDependencies)({
15
15
  config,
16
16
  input: fileToCheck,
17
17
  testSteps,
@@ -21,7 +21,7 @@ async function checkDependencies(input) {
21
21
  allErrors.join('\n'));
22
22
  }
23
23
  for (const file of allFiles) {
24
- if (!(0, planMethods_1.isFlowFile)(file)) {
24
+ if (!(0, execution_plan_utils_1.isFlowFile)(file)) {
25
25
  // js/media files don't have dependencies
26
26
  checkedDependencies.push(file);
27
27
  }
@@ -45,7 +45,7 @@ function getWorkspaceConfig(input, unfilteredFlowFiles) {
45
45
  const possibleConfigPaths = new Set([path.join(input, 'config.yaml'), path.join(input, 'config.yml')].map((p) => path.normalize(p)));
46
46
  const configFilePath = unfilteredFlowFiles.find((file) => possibleConfigPaths.has(path.normalize(file)));
47
47
  const config = configFilePath
48
- ? (0, planMethods_1.readYamlFileAsJson)(configFilePath)
48
+ ? (0, execution_plan_utils_1.readYamlFileAsJson)(configFilePath)
49
49
  : {};
50
50
  return config;
51
51
  }
@@ -75,7 +75,7 @@ async function plan(input, includeTags, excludeTags, excludeFlows, configFile, d
75
75
  normalizedInput.endsWith('config.yml')) {
76
76
  throw new Error('If using config.yaml, pass the workspace folder path, not the config file or a custom path via --config');
77
77
  }
78
- const { config } = (0, planMethods_1.readTestYamlFileAsJson)(normalizedInput);
78
+ const { config } = (0, execution_plan_utils_1.readTestYamlFileAsJson)(normalizedInput);
79
79
  const flowOverrides = {};
80
80
  if (config) {
81
81
  flowMetadata[normalizedInput] = config;
@@ -90,7 +90,7 @@ async function plan(input, includeTags, excludeTags, excludeFlows, configFile, d
90
90
  totalFlowFiles: 1,
91
91
  };
92
92
  }
93
- let unfilteredFlowFiles = await (0, planMethods_1.readDirectory)(normalizedInput, planMethods_1.isFlowFile);
93
+ let unfilteredFlowFiles = await (0, execution_plan_utils_1.readDirectory)(normalizedInput, execution_plan_utils_1.isFlowFile);
94
94
  if (unfilteredFlowFiles.length === 0) {
95
95
  throw new Error(`Flow directory does not contain any Flow files: ${path.resolve(normalizedInput)}`);
96
96
  }
@@ -101,7 +101,7 @@ async function plan(input, includeTags, excludeTags, excludeFlows, configFile, d
101
101
  if (!fs.existsSync(configFilePath)) {
102
102
  throw new Error(`Config file does not exist: ${configFilePath}`);
103
103
  }
104
- workspaceConfig = (0, planMethods_1.readYamlFileAsJson)(configFilePath);
104
+ workspaceConfig = (0, execution_plan_utils_1.readYamlFileAsJson)(configFilePath);
105
105
  }
106
106
  else {
107
107
  workspaceConfig = getWorkspaceConfig(normalizedInput, unfilteredFlowFiles);
@@ -154,7 +154,7 @@ async function plan(input, includeTags, excludeTags, excludeFlows, configFile, d
154
154
  const configPerFlowFile =
155
155
  // eslint-disable-next-line unicorn/no-array-reduce
156
156
  unfilteredFlowFiles.reduce((acc, filePath) => {
157
- const { config } = (0, planMethods_1.readTestYamlFileAsJson)(filePath);
157
+ const { config } = (0, execution_plan_utils_1.readTestYamlFileAsJson)(filePath);
158
158
  acc[filePath] = config;
159
159
  return acc;
160
160
  }, {});
@@ -207,7 +207,7 @@ async function plan(input, includeTags, excludeTags, excludeFlows, configFile, d
207
207
  if (debug && flowOrder !== normalizedFlowOrder) {
208
208
  console.log(`[DEBUG] Stripping trailing extension: "${flowOrder}" -> "${normalizedFlowOrder}"`);
209
209
  }
210
- return (0, planMethods_1.getFlowsToRunInSequence)(pathsByName, [normalizedFlowOrder], debug);
210
+ return (0, execution_plan_utils_1.getFlowsToRunInSequence)(pathsByName, [normalizedFlowOrder], debug);
211
211
  })
212
212
  .flat() || [];
213
213
  if (debug) {
@@ -4,7 +4,6 @@ exports.processDependencies = exports.checkIfFilesExistInWorkspace = exports.rea
4
4
  exports.getFlowsToRunInSequence = getFlowsToRunInSequence;
5
5
  exports.isFlowFile = isFlowFile;
6
6
  exports.readDirectory = readDirectory;
7
- /* eslint-disable unicorn/filename-case */
8
7
  const yaml = require("js-yaml");
9
8
  const fs = require("node:fs");
10
9
  const path = require("node:path");
@@ -0,0 +1,46 @@
1
+ export interface TAppMetadata {
2
+ appId: string;
3
+ platform: 'android' | 'ios';
4
+ }
5
+ /**
6
+ * Interface for platform-specific metadata extractors
7
+ */
8
+ export interface IMetadataExtractor {
9
+ canHandle(filePath: string): boolean;
10
+ extract(filePath: string): Promise<TAppMetadata>;
11
+ }
12
+ /**
13
+ * Extracts metadata from Android APK files
14
+ */
15
+ export declare class AndroidMetadataExtractor implements IMetadataExtractor {
16
+ canHandle(filePath: string): boolean;
17
+ extract(filePath: string): Promise<TAppMetadata>;
18
+ }
19
+ /**
20
+ * Extracts metadata from iOS .app directories
21
+ */
22
+ export declare class IosAppMetadataExtractor implements IMetadataExtractor {
23
+ canHandle(filePath: string): boolean;
24
+ extract(filePath: string): Promise<TAppMetadata>;
25
+ private parseInfoPlist;
26
+ }
27
+ /**
28
+ * Extracts metadata from iOS .zip files containing .app bundles
29
+ */
30
+ export declare class IosZipMetadataExtractor implements IMetadataExtractor {
31
+ canHandle(filePath: string): boolean;
32
+ extract(filePath: string): Promise<TAppMetadata>;
33
+ private parseInfoPlist;
34
+ }
35
+ /**
36
+ * Service for extracting app metadata from various file formats
37
+ */
38
+ export declare class MetadataExtractorService {
39
+ private extractors;
40
+ /**
41
+ * Extract app metadata from a file
42
+ * @param filePath - Path to the app file (.apk, .app, or .zip)
43
+ * @returns App metadata or undefined if extraction fails
44
+ */
45
+ extract(filePath: string): Promise<TAppMetadata | undefined>;
46
+ }