@testingbot/cli 1.0.2 → 1.0.4
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/README.md +82 -6
- package/dist/logger.js +4 -4
- package/dist/models/maestro_options.d.ts +1 -0
- package/dist/models/maestro_options.d.ts.map +1 -1
- package/dist/models/maestro_options.js +5 -1
- package/dist/providers/espresso.d.ts.map +1 -1
- package/dist/providers/espresso.js +13 -13
- package/dist/providers/maestro.d.ts +28 -0
- package/dist/providers/maestro.d.ts.map +1 -1
- package/dist/providers/maestro.js +353 -77
- package/dist/providers/xcuitest.d.ts.map +1 -1
- package/dist/providers/xcuitest.js +13 -13
- package/dist/upload.d.ts +11 -0
- package/dist/upload.d.ts.map +1 -1
- package/dist/upload.js +57 -0
- package/dist/utils/connectivity.d.ts +2 -1
- package/dist/utils/connectivity.d.ts.map +1 -1
- package/dist/utils/connectivity.js +83 -70
- package/dist/utils/file-type-detector.d.ts +4 -1
- package/dist/utils/file-type-detector.d.ts.map +1 -1
- package/dist/utils/file-type-detector.js +30 -6
- package/dist/utils.d.ts +18 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +58 -7
- package/package.json +3 -3
package/dist/upload.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface UploadOptions {
|
|
|
7
7
|
contentType: ContentType;
|
|
8
8
|
showProgress?: boolean;
|
|
9
9
|
checksum?: string;
|
|
10
|
+
/** Validate that the file is a valid zip-based archive (APK, IPA, ZIP) */
|
|
11
|
+
validateZipFormat?: boolean;
|
|
10
12
|
}
|
|
11
13
|
export interface UploadResult {
|
|
12
14
|
id: number;
|
|
@@ -19,6 +21,15 @@ export default class Upload {
|
|
|
19
21
|
*/
|
|
20
22
|
private formatFileSize;
|
|
21
23
|
private validateFile;
|
|
24
|
+
/**
|
|
25
|
+
* Validate that the file is a valid zip-based archive.
|
|
26
|
+
* ZIP, APK, IPA files all start with the ZIP magic bytes (PK\x03\x04).
|
|
27
|
+
*/
|
|
28
|
+
private validateZipFormat;
|
|
29
|
+
/**
|
|
30
|
+
* Extract error message from server response data
|
|
31
|
+
*/
|
|
32
|
+
private extractErrorMessage;
|
|
22
33
|
/**
|
|
23
34
|
* Calculate MD5 checksum of a file, returning base64-encoded result
|
|
24
35
|
* This matches ActiveStorage's checksum format
|
package/dist/upload.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AAMA,OAAO,WAAW,MAAM,sBAAsB,CAAC;AAK/C,MAAM,MAAM,WAAW,GACnB,yCAAyC,GACzC,0BAA0B,GAC1B,iBAAiB,CAAC;AAEtB,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,WAAW,CAAC;IACzB,WAAW,EAAE,WAAW,CAAC;IACzB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AAMA,OAAO,WAAW,MAAM,sBAAsB,CAAC;AAK/C,MAAM,MAAM,WAAW,GACnB,yCAAyC,GACzC,0BAA0B,GAC1B,iBAAiB,CAAC;AAEtB,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,WAAW,CAAC;IACzB,WAAW,EAAE,WAAW,CAAC;IACzB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,CAAC,OAAO,OAAO,MAAM;IACZ,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;IAyGlE,OAAO,CAAC,eAAe;IAmBvB;;OAEG;IACH,OAAO,CAAC,cAAc;YAUR,YAAY;IAQ1B;;;OAGG;YACW,iBAAiB;IAqB/B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAuB3B;;;OAGG;IACU,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAUlE"}
|
package/dist/upload.js
CHANGED
|
@@ -16,6 +16,9 @@ class Upload {
|
|
|
16
16
|
async upload(options) {
|
|
17
17
|
const { filePath, url, credentials, showProgress = false } = options;
|
|
18
18
|
await this.validateFile(filePath);
|
|
19
|
+
if (options.validateZipFormat) {
|
|
20
|
+
await this.validateZipFormat(filePath);
|
|
21
|
+
}
|
|
19
22
|
const fileName = node_path_1.default.basename(filePath);
|
|
20
23
|
const fileStats = await node_fs_1.default.promises.stat(filePath);
|
|
21
24
|
const totalSize = fileStats.size;
|
|
@@ -85,6 +88,11 @@ class Upload {
|
|
|
85
88
|
throw error;
|
|
86
89
|
}
|
|
87
90
|
if (axios_1.default.isAxiosError(error)) {
|
|
91
|
+
// Handle 400 errors specifically for file uploads
|
|
92
|
+
if (error.response?.status === 400) {
|
|
93
|
+
const serverMessage = this.extractErrorMessage(error.response.data);
|
|
94
|
+
throw new testingbot_error_1.default(`Upload rejected: ${serverMessage || 'The file was not accepted by the server'}`, { cause: error });
|
|
95
|
+
}
|
|
88
96
|
throw (0, error_helpers_1.handleAxiosError)(error, 'Upload failed');
|
|
89
97
|
}
|
|
90
98
|
throw new testingbot_error_1.default(`Upload failed: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error instanceof Error ? error : undefined });
|
|
@@ -122,6 +130,55 @@ class Upload {
|
|
|
122
130
|
throw new testingbot_error_1.default(`File not found or not readable: ${filePath}`);
|
|
123
131
|
}
|
|
124
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Validate that the file is a valid zip-based archive.
|
|
135
|
+
* ZIP, APK, IPA files all start with the ZIP magic bytes (PK\x03\x04).
|
|
136
|
+
*/
|
|
137
|
+
async validateZipFormat(filePath) {
|
|
138
|
+
const ZIP_MAGIC_BYTES = Buffer.from([0x50, 0x4b, 0x03, 0x04]); // PK\x03\x04
|
|
139
|
+
const fd = await node_fs_1.default.promises.open(filePath, 'r');
|
|
140
|
+
try {
|
|
141
|
+
const buffer = Buffer.alloc(4);
|
|
142
|
+
const { bytesRead } = await fd.read(buffer, 0, 4, 0);
|
|
143
|
+
if (bytesRead < 4 || !buffer.subarray(0, 4).equals(ZIP_MAGIC_BYTES)) {
|
|
144
|
+
const fileName = node_path_1.default.basename(filePath);
|
|
145
|
+
const ext = node_path_1.default.extname(filePath).toLowerCase();
|
|
146
|
+
throw new testingbot_error_1.default(`Invalid file format: "${fileName}" is not a valid ${ext || 'archive'} file. ` +
|
|
147
|
+
`The file does not appear to be a valid zip-based archive (APK, IPA, or ZIP).`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
await fd.close();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Extract error message from server response data
|
|
156
|
+
*/
|
|
157
|
+
extractErrorMessage(data) {
|
|
158
|
+
if (!data)
|
|
159
|
+
return undefined;
|
|
160
|
+
if (typeof data === 'string') {
|
|
161
|
+
try {
|
|
162
|
+
const parsed = JSON.parse(data);
|
|
163
|
+
return parsed.message || parsed.error || parsed.errors || data;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return data;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (typeof data === 'object') {
|
|
170
|
+
const obj = data;
|
|
171
|
+
if (typeof obj.message === 'string')
|
|
172
|
+
return obj.message;
|
|
173
|
+
if (typeof obj.error === 'string')
|
|
174
|
+
return obj.error;
|
|
175
|
+
if (typeof obj.errors === 'string')
|
|
176
|
+
return obj.errors;
|
|
177
|
+
if (Array.isArray(obj.errors))
|
|
178
|
+
return obj.errors.join(', ');
|
|
179
|
+
}
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
125
182
|
/**
|
|
126
183
|
* Calculate MD5 checksum of a file, returning base64-encoded result
|
|
127
184
|
* This matches ActiveStorage's checksum format
|
|
@@ -15,7 +15,8 @@ export interface ConnectivityCheckResult {
|
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
17
|
* Check if the system has internet connectivity by testing against
|
|
18
|
-
* multiple reliable third-party endpoints
|
|
18
|
+
* multiple reliable third-party endpoints in parallel.
|
|
19
|
+
* Returns as soon as one endpoint succeeds, reducing latency significantly.
|
|
19
20
|
*/
|
|
20
21
|
export declare function checkInternetConnectivity(): Promise<ConnectivityCheckResult>;
|
|
21
22
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connectivity.d.ts","sourceRoot":"","sources":["../../src/utils/connectivity.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,cAAc,EAAE,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;CACjB;
|
|
1
|
+
{"version":3,"file":"connectivity.d.ts","sourceRoot":"","sources":["../../src/utils/connectivity.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,cAAc,EAAE,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;CACjB;AA4DD;;;;GAIG;AACH,wBAAsB,yBAAyB,IAAI,OAAO,CAAC,uBAAuB,CAAC,CA4ClF;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,uBAAuB,GAC9B,MAAM,CAuBR"}
|
|
@@ -5,9 +5,66 @@
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.checkInternetConnectivity = checkInternetConnectivity;
|
|
7
7
|
exports.formatConnectivityResults = formatConnectivityResults;
|
|
8
|
+
/**
|
|
9
|
+
* Test a single endpoint and return the result
|
|
10
|
+
*/
|
|
11
|
+
async function testEndpoint(url, description) {
|
|
12
|
+
const startTime = Date.now();
|
|
13
|
+
try {
|
|
14
|
+
const controller = new AbortController();
|
|
15
|
+
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
|
16
|
+
const response = await fetch(url, {
|
|
17
|
+
method: 'HEAD',
|
|
18
|
+
signal: controller.signal,
|
|
19
|
+
redirect: 'manual',
|
|
20
|
+
});
|
|
21
|
+
clearTimeout(timeoutId);
|
|
22
|
+
const latencyMs = Date.now() - startTime;
|
|
23
|
+
return {
|
|
24
|
+
endpoint: `${description} (${url})`,
|
|
25
|
+
success: true,
|
|
26
|
+
statusCode: response.status,
|
|
27
|
+
latencyMs,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const latencyMs = Date.now() - startTime;
|
|
32
|
+
let errorMessage = 'Unknown error';
|
|
33
|
+
if (error instanceof Error) {
|
|
34
|
+
if (error.name === 'AbortError') {
|
|
35
|
+
errorMessage = 'Request timeout (>3s)';
|
|
36
|
+
}
|
|
37
|
+
else if (error.message.includes('fetch failed')) {
|
|
38
|
+
errorMessage = 'Network request failed (DNS/connection error)';
|
|
39
|
+
}
|
|
40
|
+
else if (error.message.includes('ENOTFOUND')) {
|
|
41
|
+
errorMessage = 'DNS resolution failed';
|
|
42
|
+
}
|
|
43
|
+
else if (error.message.includes('ECONNREFUSED')) {
|
|
44
|
+
errorMessage = 'Connection refused';
|
|
45
|
+
}
|
|
46
|
+
else if (error.message.includes('ETIMEDOUT')) {
|
|
47
|
+
errorMessage = 'Connection timeout';
|
|
48
|
+
}
|
|
49
|
+
else if (error.message.includes('ENETUNREACH')) {
|
|
50
|
+
errorMessage = 'Network unreachable';
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
errorMessage = error.message;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
endpoint: `${description} (${url})`,
|
|
58
|
+
success: false,
|
|
59
|
+
error: errorMessage,
|
|
60
|
+
latencyMs,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
8
64
|
/**
|
|
9
65
|
* Check if the system has internet connectivity by testing against
|
|
10
|
-
* multiple reliable third-party endpoints
|
|
66
|
+
* multiple reliable third-party endpoints in parallel.
|
|
67
|
+
* Returns as soon as one endpoint succeeds, reducing latency significantly.
|
|
11
68
|
*/
|
|
12
69
|
async function checkInternetConnectivity() {
|
|
13
70
|
const testEndpoints = [
|
|
@@ -18,79 +75,35 @@ async function checkInternetConnectivity() {
|
|
|
18
75
|
},
|
|
19
76
|
{ url: 'https://1.1.1.1/', description: 'Cloudflare DNS' },
|
|
20
77
|
];
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
signal: controller.signal,
|
|
31
|
-
redirect: 'manual',
|
|
32
|
-
});
|
|
33
|
-
clearTimeout(timeoutId);
|
|
34
|
-
const latencyMs = Date.now() - startTime;
|
|
35
|
-
if (response) {
|
|
36
|
-
anySuccess = true;
|
|
37
|
-
endpointResults.push({
|
|
38
|
-
endpoint: `${description} (${url})`,
|
|
39
|
-
success: true,
|
|
40
|
-
statusCode: response.status,
|
|
41
|
-
latencyMs,
|
|
42
|
-
});
|
|
43
|
-
break;
|
|
78
|
+
// Test all endpoints in parallel
|
|
79
|
+
const endpointPromises = testEndpoints.map(({ url, description }) => testEndpoint(url, description));
|
|
80
|
+
// Use Promise.any to return on first success, or collect all failures
|
|
81
|
+
try {
|
|
82
|
+
// Create promises that only resolve on success
|
|
83
|
+
const successPromises = endpointPromises.map(async (promise) => {
|
|
84
|
+
const result = await promise;
|
|
85
|
+
if (result.success) {
|
|
86
|
+
return result;
|
|
44
87
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
else if (error.message.includes('fetch failed')) {
|
|
54
|
-
errorMessage = 'Network request failed (DNS/connection error)';
|
|
55
|
-
}
|
|
56
|
-
else if (error.message.includes('ENOTFOUND')) {
|
|
57
|
-
errorMessage = 'DNS resolution failed';
|
|
58
|
-
}
|
|
59
|
-
else if (error.message.includes('ECONNREFUSED')) {
|
|
60
|
-
errorMessage = 'Connection refused';
|
|
61
|
-
}
|
|
62
|
-
else if (error.message.includes('ETIMEDOUT')) {
|
|
63
|
-
errorMessage = 'Connection timeout';
|
|
64
|
-
}
|
|
65
|
-
else if (error.message.includes('ENETUNREACH')) {
|
|
66
|
-
errorMessage = 'Network unreachable';
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
errorMessage = error.message;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
endpointResults.push({
|
|
73
|
-
endpoint: `${description} (${url})`,
|
|
74
|
-
success: false,
|
|
75
|
-
error: errorMessage,
|
|
76
|
-
latencyMs,
|
|
77
|
-
});
|
|
78
|
-
}
|
|
88
|
+
throw result; // Throw failures so Promise.any continues to next
|
|
89
|
+
});
|
|
90
|
+
const successResult = await Promise.any(successPromises);
|
|
91
|
+
return {
|
|
92
|
+
connected: true,
|
|
93
|
+
endpointResults: [successResult],
|
|
94
|
+
message: `Internet connectivity verified via ${successResult.endpoint} (${successResult.latencyMs}ms)`,
|
|
95
|
+
};
|
|
79
96
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
message = `Internet connectivity verified via ${successfulEndpoint?.endpoint} (${successfulEndpoint?.latencyMs}ms)`;
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
97
|
+
catch (aggregateError) {
|
|
98
|
+
// All endpoints failed - collect all results
|
|
99
|
+
const endpointResults = await Promise.all(endpointPromises);
|
|
86
100
|
const testedEndpoints = endpointResults.map((r) => r.endpoint).join(', ');
|
|
87
|
-
|
|
101
|
+
return {
|
|
102
|
+
connected: false,
|
|
103
|
+
endpointResults,
|
|
104
|
+
message: `No internet connectivity detected. Tested endpoints: ${testedEndpoints}`,
|
|
105
|
+
};
|
|
88
106
|
}
|
|
89
|
-
return {
|
|
90
|
-
connected: anySuccess,
|
|
91
|
-
endpointResults,
|
|
92
|
-
message,
|
|
93
|
-
};
|
|
94
107
|
}
|
|
95
108
|
/**
|
|
96
109
|
* Format connectivity check results for display
|
|
@@ -9,7 +9,10 @@ export interface FileTypeResult {
|
|
|
9
9
|
export declare function detectFileType(filePath: string): Promise<FileTypeResult | undefined>;
|
|
10
10
|
/**
|
|
11
11
|
* Detect platform (Android or iOS) from app file.
|
|
12
|
-
* Uses magic bytes
|
|
12
|
+
* Uses a combination of magic bytes and file extension for reliable detection.
|
|
13
|
+
*
|
|
14
|
+
* Android: .apk, .apks files
|
|
15
|
+
* iOS: .ipa, .app, .zip files (when not APK)
|
|
13
16
|
*/
|
|
14
17
|
export declare function detectPlatformFromFile(filePath: string): Promise<'Android' | 'iOS' | undefined>;
|
|
15
18
|
//# sourceMappingURL=file-type-detector.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-type-detector.d.ts","sourceRoot":"","sources":["../../src/utils/file-type-detector.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"file-type-detector.d.ts","sourceRoot":"","sources":["../../src/utils/file-type-detector.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CASrC;AASD;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,SAAS,GAAG,KAAK,GAAG,SAAS,CAAC,CAgCxC"}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.detectFileType = detectFileType;
|
|
4
7
|
exports.detectPlatformFromFile = detectPlatformFromFile;
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
5
9
|
/**
|
|
6
10
|
* Detect file type from file content using magic bytes.
|
|
7
11
|
* Returns undefined if the file type cannot be determined.
|
|
@@ -17,22 +21,42 @@ async function detectFileType(filePath) {
|
|
|
17
21
|
return undefined;
|
|
18
22
|
}
|
|
19
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Get file extension in lowercase without the dot
|
|
26
|
+
*/
|
|
27
|
+
function getExtension(filePath) {
|
|
28
|
+
return node_path_1.default.extname(filePath).toLowerCase().slice(1);
|
|
29
|
+
}
|
|
20
30
|
/**
|
|
21
31
|
* Detect platform (Android or iOS) from app file.
|
|
22
|
-
* Uses magic bytes
|
|
32
|
+
* Uses a combination of magic bytes and file extension for reliable detection.
|
|
33
|
+
*
|
|
34
|
+
* Android: .apk, .apks files
|
|
35
|
+
* iOS: .ipa, .app, .zip files (when not APK)
|
|
23
36
|
*/
|
|
24
37
|
async function detectPlatformFromFile(filePath) {
|
|
38
|
+
const ext = getExtension(filePath);
|
|
25
39
|
const fileType = await detectFileType(filePath);
|
|
40
|
+
// Check for Android APK files
|
|
26
41
|
if (fileType) {
|
|
27
|
-
// APK files are detected as 'application/zip' with ext 'apk'
|
|
28
|
-
// or as 'application/vnd.android.package-archive'
|
|
29
42
|
if (fileType.ext === 'apk' ||
|
|
30
43
|
fileType.mime === 'application/vnd.android.package-archive') {
|
|
31
44
|
return 'Android';
|
|
32
45
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
}
|
|
47
|
+
// Extension-based detection (more reliable for mobile apps)
|
|
48
|
+
// APK and APKS are Android
|
|
49
|
+
if (ext === 'apk' || ext === 'apks') {
|
|
50
|
+
return 'Android';
|
|
51
|
+
}
|
|
52
|
+
// IPA, APP are iOS
|
|
53
|
+
if (ext === 'ipa' || ext === 'app') {
|
|
54
|
+
return 'iOS';
|
|
55
|
+
}
|
|
56
|
+
// ZIP files could be either, but commonly used for iOS simulator builds
|
|
57
|
+
// If magic bytes detected it as zip and extension is .zip, assume iOS
|
|
58
|
+
if (ext === 'zip' && fileType?.mime === 'application/zip') {
|
|
59
|
+
return 'iOS';
|
|
36
60
|
}
|
|
37
61
|
return undefined;
|
|
38
62
|
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -6,6 +6,24 @@ declare const _default: {
|
|
|
6
6
|
* Returns: -1 if v1 < v2, 0 if equal, 1 if v1 > v2
|
|
7
7
|
*/
|
|
8
8
|
compareVersions(v1: string, v2: string): number;
|
|
9
|
+
/**
|
|
10
|
+
* Check if a device specification is a wildcard or regex pattern
|
|
11
|
+
*/
|
|
12
|
+
isWildcardDevice(device: string | undefined): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Check if a version specification is a wildcard or regex pattern
|
|
15
|
+
*/
|
|
16
|
+
isWildcardVersion(version: string | undefined): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Show info message when running many flows on a specific real device without sharding
|
|
19
|
+
*/
|
|
20
|
+
showRealDeviceFlowsInfo(options: {
|
|
21
|
+
realDevice: boolean;
|
|
22
|
+
device?: string;
|
|
23
|
+
version?: string;
|
|
24
|
+
flowCount: number;
|
|
25
|
+
shardSplit?: number;
|
|
26
|
+
}): void;
|
|
9
27
|
/**
|
|
10
28
|
* Check if a newer version is available and display update notice
|
|
11
29
|
*/
|
package/dist/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";oBAQkB,MAAM;yBAID,MAAM;IAI3B;;;OAGG;wBACiB,MAAM,MAAM,MAAM,GAAG,MAAM;IAa/C;;OAEG;6BACsB,MAAM,GAAG,SAAS,GAAG,OAAO;IAMrD;;OAEG;+BACwB,MAAM,GAAG,SAAS,GAAG,OAAO;IAMvD;;OAEG;qCAC8B;QAC/B,UAAU,EAAE,OAAO,CAAC;QACpB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,IAAI;IAuDR;;OAEG;kCAC2B,MAAM,GAAG,SAAS,GAAG,IAAI;;AA/GzD,wBAyIE"}
|
package/dist/utils.js
CHANGED
|
@@ -5,8 +5,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const package_json_1 = __importDefault(require("../package.json"));
|
|
7
7
|
const logger_1 = __importDefault(require("./logger"));
|
|
8
|
-
const
|
|
8
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
9
9
|
let versionCheckDisplayed = false;
|
|
10
|
+
let realDeviceFlowsInfoDisplayed = false;
|
|
10
11
|
exports.default = {
|
|
11
12
|
getUserAgent() {
|
|
12
13
|
return `TestingBot-CTL-${package_json_1.default.version}`;
|
|
@@ -31,6 +32,56 @@ exports.default = {
|
|
|
31
32
|
}
|
|
32
33
|
return 0;
|
|
33
34
|
},
|
|
35
|
+
/**
|
|
36
|
+
* Check if a device specification is a wildcard or regex pattern
|
|
37
|
+
*/
|
|
38
|
+
isWildcardDevice(device) {
|
|
39
|
+
if (!device)
|
|
40
|
+
return true;
|
|
41
|
+
// Check for common wildcard/regex characters
|
|
42
|
+
return device === '*' || device.includes('*') || device.includes('?') || device.includes('.*');
|
|
43
|
+
},
|
|
44
|
+
/**
|
|
45
|
+
* Check if a version specification is a wildcard or regex pattern
|
|
46
|
+
*/
|
|
47
|
+
isWildcardVersion(version) {
|
|
48
|
+
if (!version)
|
|
49
|
+
return true;
|
|
50
|
+
// Check for common wildcard/regex characters
|
|
51
|
+
return version === '*' || version.includes('*') || version.includes('?') || version.includes('.*');
|
|
52
|
+
},
|
|
53
|
+
/**
|
|
54
|
+
* Show info message when running many flows on a specific real device without sharding
|
|
55
|
+
*/
|
|
56
|
+
showRealDeviceFlowsInfo(options) {
|
|
57
|
+
// Only show once
|
|
58
|
+
if (realDeviceFlowsInfoDisplayed) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Check conditions: real device, specific device, more than 2 flows, no shards
|
|
62
|
+
if (!options.realDevice ||
|
|
63
|
+
this.isWildcardDevice(options.device) ||
|
|
64
|
+
options.flowCount <= 2 ||
|
|
65
|
+
options.shardSplit) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
realDeviceFlowsInfoDisplayed = true;
|
|
69
|
+
const border = '─'.repeat(80);
|
|
70
|
+
logger_1.default.info('');
|
|
71
|
+
logger_1.default.info(picocolors_1.default.cyan(border));
|
|
72
|
+
logger_1.default.info(picocolors_1.default.cyan('ℹ Performance Tip'));
|
|
73
|
+
logger_1.default.info(picocolors_1.default.cyan(` Running ${options.flowCount} flows on a specific device (${options.device}) in real device mode.`));
|
|
74
|
+
logger_1.default.info(picocolors_1.default.cyan(' Each flow runs in its own session on that device, which may be slow.'));
|
|
75
|
+
logger_1.default.info(picocolors_1.default.cyan(''));
|
|
76
|
+
logger_1.default.info(picocolors_1.default.cyan(' Consider these alternatives for faster execution:'));
|
|
77
|
+
logger_1.default.info(picocolors_1.default.cyan(` • Use ${picocolors_1.default.white('--shard-split <n>')} to run multiple flows in the same session`));
|
|
78
|
+
logger_1.default.info(picocolors_1.default.cyan(` • Use wildcards for device (e.g., ${picocolors_1.default.white('"Pixel.*"')}) to parallelize across devices`));
|
|
79
|
+
if (!this.isWildcardVersion(options.version)) {
|
|
80
|
+
logger_1.default.info(picocolors_1.default.cyan(` • Use wildcards for version (e.g., ${picocolors_1.default.white('"15.*"')}) for broader device selection`));
|
|
81
|
+
}
|
|
82
|
+
logger_1.default.info(picocolors_1.default.cyan(border));
|
|
83
|
+
logger_1.default.info('');
|
|
84
|
+
},
|
|
34
85
|
/**
|
|
35
86
|
* Check if a newer version is available and display update notice
|
|
36
87
|
*/
|
|
@@ -42,12 +93,12 @@ exports.default = {
|
|
|
42
93
|
if (this.compareVersions(currentVersion, latestVersion) < 0) {
|
|
43
94
|
versionCheckDisplayed = true;
|
|
44
95
|
const border = '─'.repeat(80);
|
|
45
|
-
logger_1.default.info(`\nCLI Version: ${
|
|
46
|
-
logger_1.default.warn(
|
|
47
|
-
logger_1.default.warn(
|
|
48
|
-
logger_1.default.warn(
|
|
49
|
-
logger_1.default.warn(
|
|
50
|
-
logger_1.default.warn(
|
|
96
|
+
logger_1.default.info(`\nCLI Version: ${picocolors_1.default.cyan(currentVersion)}\n`);
|
|
97
|
+
logger_1.default.warn(picocolors_1.default.yellow(border));
|
|
98
|
+
logger_1.default.warn(picocolors_1.default.yellow('⚠ Update Available'));
|
|
99
|
+
logger_1.default.warn(picocolors_1.default.yellow(` A new version of the TestingBot CLI is available: ${picocolors_1.default.green(latestVersion)}`));
|
|
100
|
+
logger_1.default.warn(picocolors_1.default.yellow(` Run: ${picocolors_1.default.cyan('npm install -g @testingbot/cli@latest')}`));
|
|
101
|
+
logger_1.default.warn(picocolors_1.default.yellow(border) + '\n');
|
|
51
102
|
}
|
|
52
103
|
},
|
|
53
104
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testingbot/cli",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "CLI tool to run Espresso, XCUITest
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"description": "CLI tool to run Espresso, XCUITest and Maestro tests on TestingBot's cloud infrastructure",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"testingbot": "dist/index.js"
|
|
@@ -49,12 +49,12 @@
|
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"archiver": "^7.0.1",
|
|
51
51
|
"axios": "^1.13.2",
|
|
52
|
-
"colors": "^1.4.0",
|
|
53
52
|
"commander": "^14.0.2",
|
|
54
53
|
"file-type": "^21.1.1",
|
|
55
54
|
"form-data": "^4.0.5",
|
|
56
55
|
"glob": "^13.0.0",
|
|
57
56
|
"js-yaml": "^4.1.1",
|
|
57
|
+
"picocolors": "^1.1.1",
|
|
58
58
|
"progress-stream": "^2.0.0",
|
|
59
59
|
"socket.io-client": "^4.8.1",
|
|
60
60
|
"tracer": "^1.3.0"
|