@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.
- package/dist/commands/cloud.d.ts +26 -34
- package/dist/commands/cloud.js +117 -465
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.js +19 -1
- package/dist/config/flags/api.flags.d.ts +7 -0
- package/dist/config/flags/api.flags.js +19 -0
- package/dist/config/flags/binary.flags.d.ts +8 -0
- package/dist/config/flags/binary.flags.js +20 -0
- package/dist/config/flags/device.flags.d.ts +14 -0
- package/dist/config/flags/device.flags.js +46 -0
- package/dist/config/flags/environment.flags.d.ts +11 -0
- package/dist/config/flags/environment.flags.js +37 -0
- package/dist/config/flags/execution.flags.d.ts +13 -0
- package/dist/config/flags/execution.flags.js +50 -0
- package/dist/config/flags/output.flags.d.ts +18 -0
- package/dist/config/flags/output.flags.js +61 -0
- package/dist/constants.d.ts +28 -24
- package/dist/constants.js +21 -206
- package/dist/gateways/api-gateway.d.ts +3 -3
- package/dist/methods.d.ts +0 -4
- package/dist/methods.js +15 -80
- package/dist/services/device-validation.service.d.ts +29 -0
- package/dist/services/device-validation.service.js +72 -0
- package/dist/{plan.d.ts → services/execution-plan.service.d.ts} +1 -1
- package/dist/{plan.js → services/execution-plan.service.js} +10 -10
- package/dist/{planMethods.js → services/execution-plan.utils.js} +0 -1
- package/dist/services/metadata-extractor.service.d.ts +46 -0
- package/dist/services/metadata-extractor.service.js +138 -0
- package/dist/services/moropo.service.d.ts +20 -0
- package/dist/services/moropo.service.js +113 -0
- package/dist/services/report-download.service.d.ts +40 -0
- package/dist/services/report-download.service.js +110 -0
- package/dist/services/results-polling.service.d.ts +45 -0
- package/dist/services/results-polling.service.js +210 -0
- package/dist/services/test-submission.service.d.ts +41 -0
- package/dist/services/test-submission.service.js +116 -0
- package/dist/services/version.service.d.ts +31 -0
- package/dist/services/version.service.js +81 -0
- package/dist/types/{schema.types.d.ts → generated/schema.types.d.ts} +349 -349
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.js +24 -0
- package/dist/utils/compatibility.d.ts +5 -0
- package/dist/utils/connectivity.d.ts +29 -0
- package/dist/utils/connectivity.js +100 -0
- package/oclif.manifest.json +195 -209
- package/package.json +2 -9
- /package/dist/{planMethods.d.ts → services/execution-plan.utils.d.ts} +0 -0
- /package/dist/types/{device.types.d.ts → domain/device.types.d.ts} +0 -0
- /package/dist/types/{device.types.js → domain/device.types.js} +0 -0
- /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 =
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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.
|
|
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
|
|
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:
|
|
93
|
+
type: mimeTypeLookupByExtension[filePath.split('.').pop()],
|
|
152
94
|
});
|
|
153
|
-
file = new
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
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,
|
|
14
|
-
const { allErrors, allFiles } = (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
+
}
|