@testingbot/cli 1.0.7 → 1.0.8
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 +25 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +75 -46
- package/dist/config/constants.d.ts +15 -0
- package/dist/config/constants.d.ts.map +1 -0
- package/dist/config/constants.js +17 -0
- package/dist/index.js +2 -0
- package/dist/models/espresso_options.d.ts +7 -0
- package/dist/models/espresso_options.d.ts.map +1 -1
- package/dist/models/espresso_options.js +18 -0
- package/dist/models/maestro_options.d.ts +12 -2
- package/dist/models/maestro_options.d.ts.map +1 -1
- package/dist/models/maestro_options.js +24 -1
- package/dist/models/xcuitest_options.d.ts +7 -0
- package/dist/models/xcuitest_options.d.ts.map +1 -1
- package/dist/models/xcuitest_options.js +18 -0
- package/dist/providers/base_provider.d.ts +28 -2
- package/dist/providers/base_provider.d.ts.map +1 -1
- package/dist/providers/base_provider.js +70 -2
- package/dist/providers/espresso.d.ts +1 -0
- package/dist/providers/espresso.d.ts.map +1 -1
- package/dist/providers/espresso.js +82 -35
- package/dist/providers/maestro.d.ts +13 -0
- package/dist/providers/maestro.d.ts.map +1 -1
- package/dist/providers/maestro.js +218 -74
- package/dist/providers/xcuitest.d.ts +1 -0
- package/dist/providers/xcuitest.d.ts.map +1 -1
- package/dist/providers/xcuitest.js +79 -35
- package/dist/ui/banner.d.ts +3 -0
- package/dist/ui/banner.d.ts.map +1 -0
- package/dist/ui/banner.js +82 -0
- package/dist/ui/spinner.d.ts +32 -0
- package/dist/ui/spinner.d.ts.map +1 -0
- package/dist/ui/spinner.js +92 -0
- package/dist/ui/terminal-title.d.ts +8 -0
- package/dist/ui/terminal-title.d.ts.map +1 -0
- package/dist/ui/terminal-title.js +57 -0
- package/dist/upload.d.ts +4 -0
- package/dist/upload.d.ts.map +1 -1
- package/dist/upload.js +70 -12
- package/dist/utils/connectivity.js +5 -3
- package/dist/utils.d.ts +6 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +10 -0
- package/package.json +5 -3
package/dist/upload.js
CHANGED
|
@@ -26,14 +26,34 @@ class Upload {
|
|
|
26
26
|
length: totalSize,
|
|
27
27
|
time: 100, // Emit progress every 100ms
|
|
28
28
|
});
|
|
29
|
-
|
|
29
|
+
const interactive = utils_1.default.isInteractive();
|
|
30
|
+
let lastPercent = -1;
|
|
31
|
+
let lastBucket = -1;
|
|
32
|
+
const BUCKET_SIZE = 10;
|
|
30
33
|
if (showProgress) {
|
|
31
|
-
|
|
34
|
+
if (interactive) {
|
|
35
|
+
this.drawProgressBar(fileName, totalSize, 0);
|
|
36
|
+
}
|
|
32
37
|
progressTracker.on('progress', (prog) => {
|
|
33
38
|
const percent = Math.round(prog.percentage);
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
if (interactive) {
|
|
40
|
+
// Redraw whenever percent changes; speed/ETA still update because
|
|
41
|
+
// progress-stream emits every 100ms.
|
|
42
|
+
if (percent !== lastPercent) {
|
|
43
|
+
lastPercent = percent;
|
|
44
|
+
this.drawProgressBar(fileName, totalSize, percent, prog.speed, prog.eta);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// Non-TTY: one line per 10% bucket, no ANSI, no \r. 100% is
|
|
49
|
+
// reported by the success path.
|
|
50
|
+
const bucket = Math.floor(percent / BUCKET_SIZE);
|
|
51
|
+
if (bucket !== lastBucket && percent < 100) {
|
|
52
|
+
lastBucket = bucket;
|
|
53
|
+
const transferred = this.formatFileSize((percent / 100) * totalSize);
|
|
54
|
+
const total = this.formatFileSize(totalSize);
|
|
55
|
+
console.log(` ${fileName}: ${percent}% (${transferred}/${total})`);
|
|
56
|
+
}
|
|
37
57
|
}
|
|
38
58
|
});
|
|
39
59
|
}
|
|
@@ -68,21 +88,36 @@ class Upload {
|
|
|
68
88
|
const result = response.data;
|
|
69
89
|
if (result.id) {
|
|
70
90
|
if (showProgress) {
|
|
71
|
-
|
|
72
|
-
|
|
91
|
+
if (interactive) {
|
|
92
|
+
this.drawProgressBar(fileName, totalSize, 100);
|
|
93
|
+
console.log('');
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
console.log(` ${fileName}: done (${this.formatFileSize(totalSize)})`);
|
|
97
|
+
}
|
|
73
98
|
}
|
|
74
99
|
return { id: result.id };
|
|
75
100
|
}
|
|
76
101
|
else {
|
|
77
102
|
if (showProgress) {
|
|
78
|
-
|
|
103
|
+
if (interactive) {
|
|
104
|
+
console.log(' Failed');
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
console.log(` ${fileName}: failed`);
|
|
108
|
+
}
|
|
79
109
|
}
|
|
80
110
|
throw new testingbot_error_1.default(`Upload failed: ${result.error || 'Unknown error'}`);
|
|
81
111
|
}
|
|
82
112
|
}
|
|
83
113
|
catch (error) {
|
|
84
114
|
if (showProgress) {
|
|
85
|
-
|
|
115
|
+
if (interactive) {
|
|
116
|
+
console.log(' Failed');
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
console.log(` ${fileName}: failed`);
|
|
120
|
+
}
|
|
86
121
|
}
|
|
87
122
|
if (error instanceof testingbot_error_1.default) {
|
|
88
123
|
throw error;
|
|
@@ -98,7 +133,7 @@ class Upload {
|
|
|
98
133
|
throw new testingbot_error_1.default(`Upload failed: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error instanceof Error ? error : undefined });
|
|
99
134
|
}
|
|
100
135
|
}
|
|
101
|
-
drawProgressBar(fileName, totalBytes, percent) {
|
|
136
|
+
drawProgressBar(fileName, totalBytes, percent, bytesPerSec, etaSeconds) {
|
|
102
137
|
const barWidth = 30;
|
|
103
138
|
const filled = Math.round((barWidth * percent) / 100);
|
|
104
139
|
const empty = barWidth - filled;
|
|
@@ -106,14 +141,27 @@ class Upload {
|
|
|
106
141
|
const transferredBytes = (percent / 100) * totalBytes;
|
|
107
142
|
const transferred = this.formatFileSize(transferredBytes);
|
|
108
143
|
const total = this.formatFileSize(totalBytes);
|
|
109
|
-
|
|
144
|
+
let suffix = '';
|
|
145
|
+
if (typeof bytesPerSec === 'number' &&
|
|
146
|
+
Number.isFinite(bytesPerSec) &&
|
|
147
|
+
bytesPerSec > 0) {
|
|
148
|
+
suffix += ` • ${this.formatFileSize(bytesPerSec)}/s`;
|
|
149
|
+
}
|
|
150
|
+
if (typeof etaSeconds === 'number' &&
|
|
151
|
+
Number.isFinite(etaSeconds) &&
|
|
152
|
+
etaSeconds > 0) {
|
|
153
|
+
suffix += ` • ETA ${this.formatDuration(etaSeconds)}`;
|
|
154
|
+
}
|
|
155
|
+
// `\x1b[K` clears from cursor to end of line so residue from a longer
|
|
156
|
+
// previous frame (e.g. "ETA 16s" → "ETA 1s") can't leak through.
|
|
157
|
+
process.stdout.write(`\r ${fileName}: [${bar}] ${percent}% (${transferred}/${total})${suffix}\x1b[K`);
|
|
110
158
|
}
|
|
111
159
|
/**
|
|
112
160
|
* Format file size in human-readable format (KB for small files, MB for larger)
|
|
113
161
|
*/
|
|
114
162
|
formatFileSize(bytes) {
|
|
115
163
|
if (bytes < 1024) {
|
|
116
|
-
return `${bytes} B`;
|
|
164
|
+
return `${Math.round(bytes)} B`;
|
|
117
165
|
}
|
|
118
166
|
else if (bytes < 1024 * 1024) {
|
|
119
167
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
@@ -122,6 +170,16 @@ class Upload {
|
|
|
122
170
|
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
123
171
|
}
|
|
124
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Format seconds as compact duration: "12s" or "3m04s".
|
|
175
|
+
*/
|
|
176
|
+
formatDuration(seconds) {
|
|
177
|
+
if (seconds < 60)
|
|
178
|
+
return `${Math.round(seconds)}s`;
|
|
179
|
+
const m = Math.floor(seconds / 60);
|
|
180
|
+
const s = Math.round(seconds % 60);
|
|
181
|
+
return `${m}m${s.toString().padStart(2, '0')}s`;
|
|
182
|
+
}
|
|
125
183
|
async validateFile(filePath) {
|
|
126
184
|
try {
|
|
127
185
|
await node_fs_1.default.promises.access(filePath, node_fs_1.default.constants.R_OK);
|
|
@@ -10,15 +10,14 @@ exports.formatConnectivityResults = formatConnectivityResults;
|
|
|
10
10
|
*/
|
|
11
11
|
async function testEndpoint(url, description) {
|
|
12
12
|
const startTime = Date.now();
|
|
13
|
+
const controller = new AbortController();
|
|
14
|
+
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
|
13
15
|
try {
|
|
14
|
-
const controller = new AbortController();
|
|
15
|
-
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
|
16
16
|
const response = await fetch(url, {
|
|
17
17
|
method: 'HEAD',
|
|
18
18
|
signal: controller.signal,
|
|
19
19
|
redirect: 'manual',
|
|
20
20
|
});
|
|
21
|
-
clearTimeout(timeoutId);
|
|
22
21
|
const latencyMs = Date.now() - startTime;
|
|
23
22
|
return {
|
|
24
23
|
endpoint: `${description} (${url})`,
|
|
@@ -60,6 +59,9 @@ async function testEndpoint(url, description) {
|
|
|
60
59
|
latencyMs,
|
|
61
60
|
};
|
|
62
61
|
}
|
|
62
|
+
finally {
|
|
63
|
+
clearTimeout(timeoutId);
|
|
64
|
+
}
|
|
63
65
|
}
|
|
64
66
|
/**
|
|
65
67
|
* Check if the system has internet connectivity by testing against
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
declare const _default: {
|
|
2
2
|
getUserAgent(): string;
|
|
3
|
+
/**
|
|
4
|
+
* True when stdout is attached to an interactive terminal. CI environments
|
|
5
|
+
* (CI=truthy) are always treated as non-interactive so progress animations
|
|
6
|
+
* don't spam log aggregators.
|
|
7
|
+
*/
|
|
8
|
+
isInteractive(): boolean;
|
|
3
9
|
getCurrentVersion(): string;
|
|
4
10
|
/**
|
|
5
11
|
* Compare two semver version strings
|
package/dist/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";oBAQkB,MAAM;
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";oBAQkB,MAAM;IAItB;;;;OAIG;qBACc,OAAO;yBAKH,MAAM;IAI3B;;;OAGG;wBACiB,MAAM,MAAM,MAAM,GAAG,MAAM;IAa/C;;OAEG;6BACsB,MAAM,GAAG,SAAS,GAAG,OAAO;IAWrD;;OAEG;+BACwB,MAAM,GAAG,SAAS,GAAG,OAAO;IAWvD;;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;IAyDR;;OAEG;kCAC2B,MAAM,GAAG,SAAS,GAAG,IAAI;;AArIzD,wBA+JE"}
|
package/dist/utils.js
CHANGED
|
@@ -12,6 +12,16 @@ exports.default = {
|
|
|
12
12
|
getUserAgent() {
|
|
13
13
|
return `TestingBot-CTL-${package_json_1.default.version}`;
|
|
14
14
|
},
|
|
15
|
+
/**
|
|
16
|
+
* True when stdout is attached to an interactive terminal. CI environments
|
|
17
|
+
* (CI=truthy) are always treated as non-interactive so progress animations
|
|
18
|
+
* don't spam log aggregators.
|
|
19
|
+
*/
|
|
20
|
+
isInteractive() {
|
|
21
|
+
if (process.env.CI)
|
|
22
|
+
return false;
|
|
23
|
+
return Boolean(process.stdout.isTTY);
|
|
24
|
+
},
|
|
15
25
|
getCurrentVersion() {
|
|
16
26
|
return package_json_1.default.version;
|
|
17
27
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testingbot/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
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": {
|
|
@@ -16,10 +16,11 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"lint": "prettier --check '**/*.{js,ts}' && eslint src/",
|
|
18
18
|
"build": "tsc",
|
|
19
|
-
"clean": "
|
|
19
|
+
"clean": "node -e \"require('fs').rmSync('dist', {recursive: true, force: true})\"",
|
|
20
20
|
"start": "node dist/index.js",
|
|
21
21
|
"format": "prettier --write '**/*.{js,ts}'",
|
|
22
|
-
"test": "jest"
|
|
22
|
+
"test": "jest",
|
|
23
|
+
"test:coverage": "jest --coverage"
|
|
23
24
|
},
|
|
24
25
|
"keywords": [
|
|
25
26
|
"testingbot",
|
|
@@ -57,6 +58,7 @@
|
|
|
57
58
|
"picocolors": "^1.1.1",
|
|
58
59
|
"progress-stream": "^2.0.0",
|
|
59
60
|
"socket.io-client": "^4.8.1",
|
|
61
|
+
"testingbot-tunnel-launcher": "^1.1.18",
|
|
60
62
|
"tracer": "^1.3.0"
|
|
61
63
|
},
|
|
62
64
|
"devDependencies": {
|