@testingbot/cli 1.0.0 → 1.0.1
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/cli.d.ts.map +1 -1
- package/dist/cli.js +15 -3
- package/dist/providers/login.js +1 -1
- package/dist/providers/maestro.d.ts +1 -1
- package/dist/providers/maestro.d.ts.map +1 -1
- package/dist/providers/maestro.js +76 -30
- package/dist/upload.d.ts +1 -4
- package/dist/upload.d.ts.map +1 -1
- package/dist/upload.js +53 -33
- package/package.json +3 -1
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAwBpC,QAAA,MAAM,OAAO,SAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAwBpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAyf9B,eAAe,OAAO,CAAC"}
|
package/dist/cli.js
CHANGED
|
@@ -101,7 +101,11 @@ const espressoCommand = program
|
|
|
101
101
|
apiSecret: args.apiSecret,
|
|
102
102
|
});
|
|
103
103
|
if (credentials === null) {
|
|
104
|
-
throw new Error('
|
|
104
|
+
throw new Error('No TestingBot credentials found. Please authenticate using one of these methods:\n' +
|
|
105
|
+
' 1. Run "testingbot login" to authenticate via browser (recommended)\n' +
|
|
106
|
+
' 2. Use --api-key and --api-secret options\n' +
|
|
107
|
+
' 3. Set TB_KEY and TB_SECRET environment variables\n' +
|
|
108
|
+
' 4. Create ~/.testingbot file with content: key:secret');
|
|
105
109
|
}
|
|
106
110
|
const espresso = new espresso_1.default(credentials, options);
|
|
107
111
|
const result = await espresso.run();
|
|
@@ -215,7 +219,11 @@ const maestroCommand = program
|
|
|
215
219
|
apiSecret: args.apiSecret,
|
|
216
220
|
});
|
|
217
221
|
if (credentials === null) {
|
|
218
|
-
throw new Error('
|
|
222
|
+
throw new Error('No TestingBot credentials found. Please authenticate using one of these methods:\n' +
|
|
223
|
+
' 1. Run "testingbot login" to authenticate via browser (recommended)\n' +
|
|
224
|
+
' 2. Use --api-key and --api-secret options\n' +
|
|
225
|
+
' 3. Set TB_KEY and TB_SECRET environment variables\n' +
|
|
226
|
+
' 4. Create ~/.testingbot file with content: key:secret');
|
|
219
227
|
}
|
|
220
228
|
const maestro = new maestro_1.default(credentials, options);
|
|
221
229
|
const result = await maestro.run();
|
|
@@ -296,7 +304,11 @@ const xcuitestCommand = program
|
|
|
296
304
|
apiSecret: args.apiSecret,
|
|
297
305
|
});
|
|
298
306
|
if (credentials === null) {
|
|
299
|
-
throw new Error('
|
|
307
|
+
throw new Error('No TestingBot credentials found. Please authenticate using one of these methods:\n' +
|
|
308
|
+
' 1. Run "testingbot login" to authenticate via browser (recommended)\n' +
|
|
309
|
+
' 2. Use --api-key and --api-secret options\n' +
|
|
310
|
+
' 3. Set TB_KEY and TB_SECRET environment variables\n' +
|
|
311
|
+
' 4. Create ~/.testingbot file with content: key:secret');
|
|
300
312
|
}
|
|
301
313
|
const xcuitest = new xcuitest_1.default(credentials, options);
|
|
302
314
|
const result = await xcuitest.run();
|
package/dist/providers/login.js
CHANGED
|
@@ -20,7 +20,7 @@ class Login {
|
|
|
20
20
|
// Open browser to auth URL
|
|
21
21
|
const authUrl = `${AUTH_URL}?port=${this.port}&identifier=testingbotctl`;
|
|
22
22
|
logger_1.default.info('Opening browser for authentication...');
|
|
23
|
-
logger_1.default.info(
|
|
23
|
+
logger_1.default.info(`If the browser does not open automatically, visit:\n\n ${authUrl}\n`);
|
|
24
24
|
await this.openBrowser(authUrl);
|
|
25
25
|
// Wait for callback (handled by server)
|
|
26
26
|
const credentials = await this.waitForCallback();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"maestro.d.ts","sourceRoot":"","sources":["../../src/providers/maestro.ts"],"names":[],"mappings":"AAAA,OAAO,cAAiC,MAAM,2BAA2B,CAAC;AAE1E,OAAO,WAAW,MAAM,uBAAuB,CAAC;AAehD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"maestro.d.ts","sourceRoot":"","sources":["../../src/providers/maestro.ts"],"names":[],"mappings":"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"}
|
|
@@ -523,7 +523,6 @@ class Maestro {
|
|
|
523
523
|
const statusEmoji = run.success === 1 ? '✅' : '❌';
|
|
524
524
|
const statusText = run.success === 1 ? 'Test completed successfully' : 'Test failed';
|
|
525
525
|
console.log(` ${statusEmoji} Run ${run.id} (${run.capabilities.deviceName}): ${statusText}`);
|
|
526
|
-
console.log(` View results: https://testingbot.com/members/maestro/${this.appId}/runs/${run.id}`);
|
|
527
526
|
}
|
|
528
527
|
}
|
|
529
528
|
const allSucceeded = status.runs.every((run) => run.success === 1);
|
|
@@ -544,7 +543,7 @@ class Maestro {
|
|
|
544
543
|
await this.fetchReports(status.runs);
|
|
545
544
|
}
|
|
546
545
|
// Download artifacts if requested
|
|
547
|
-
if (this.options.downloadArtifacts
|
|
546
|
+
if (this.options.downloadArtifacts) {
|
|
548
547
|
await this.downloadArtifacts(status.runs);
|
|
549
548
|
}
|
|
550
549
|
return {
|
|
@@ -569,6 +568,11 @@ class Maestro {
|
|
|
569
568
|
(prevStatus === 'WAITING' || prevStatus === 'READY')) {
|
|
570
569
|
this.clearLine();
|
|
571
570
|
}
|
|
571
|
+
// Show URL when test starts running (transitions from WAITING to READY)
|
|
572
|
+
if (statusChanged && prevStatus === 'WAITING' && run.status === 'READY') {
|
|
573
|
+
console.log(` 🚀 Run ${run.id} (${run.capabilities.deviceName}): Test started`);
|
|
574
|
+
console.log(` Watch this test in realtime: https://testingbot.com/members/maestro/${this.appId}/runs/${run.id}`);
|
|
575
|
+
}
|
|
572
576
|
previousStatus.set(run.id, run.status);
|
|
573
577
|
const statusInfo = this.getStatusInfo(run.status);
|
|
574
578
|
if (run.status === 'WAITING' || run.status === 'READY') {
|
|
@@ -680,31 +684,73 @@ class Maestro {
|
|
|
680
684
|
}
|
|
681
685
|
throw new testingbot_error_1.default(`Timed out waiting for artifacts to sync for run ${runId}`);
|
|
682
686
|
}
|
|
683
|
-
async downloadFile(url, filePath) {
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
'
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
687
|
+
async downloadFile(url, filePath, retries = 3) {
|
|
688
|
+
let lastError;
|
|
689
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
690
|
+
try {
|
|
691
|
+
const response = await axios_1.default.get(url, {
|
|
692
|
+
responseType: 'arraybuffer',
|
|
693
|
+
timeout: 60000, // 60 second timeout for large files
|
|
694
|
+
});
|
|
695
|
+
await node_fs_1.default.promises.writeFile(filePath, response.data);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
catch (error) {
|
|
699
|
+
lastError = error;
|
|
700
|
+
// Don't retry on 4xx errors (client errors like 403, 404)
|
|
701
|
+
if (axios_1.default.isAxiosError(error) && error.response?.status) {
|
|
702
|
+
const status = error.response.status;
|
|
703
|
+
if (status >= 400 && status < 500) {
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
// Wait before retrying (exponential backoff)
|
|
708
|
+
if (attempt < retries) {
|
|
709
|
+
await this.sleep(1000 * attempt);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
692
712
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
713
|
+
// Extract detailed error message
|
|
714
|
+
let errorDetail = '';
|
|
715
|
+
if (axios_1.default.isAxiosError(lastError)) {
|
|
716
|
+
if (lastError.response) {
|
|
717
|
+
errorDetail = `HTTP ${lastError.response.status}: ${lastError.response.statusText}`;
|
|
718
|
+
}
|
|
719
|
+
else if (lastError.code) {
|
|
720
|
+
errorDetail = lastError.code;
|
|
721
|
+
}
|
|
722
|
+
else if (lastError.message) {
|
|
723
|
+
errorDetail = lastError.message;
|
|
724
|
+
}
|
|
697
725
|
}
|
|
726
|
+
else if (lastError instanceof Error) {
|
|
727
|
+
errorDetail = lastError.message;
|
|
728
|
+
}
|
|
729
|
+
else if (lastError) {
|
|
730
|
+
errorDetail = String(lastError);
|
|
731
|
+
}
|
|
732
|
+
throw new testingbot_error_1.default(`Failed to download file${errorDetail ? `: ${errorDetail}` : ''}`, {
|
|
733
|
+
cause: lastError,
|
|
734
|
+
});
|
|
698
735
|
}
|
|
699
|
-
generateArtifactZipName() {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
const
|
|
703
|
-
return
|
|
736
|
+
async generateArtifactZipName(outputDir) {
|
|
737
|
+
if (!this.options.build) {
|
|
738
|
+
// Generate unique name with timestamp
|
|
739
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
740
|
+
return `maestro_artifacts_${timestamp}.zip`;
|
|
741
|
+
}
|
|
742
|
+
const baseName = this.options.build.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
743
|
+
const fileName = `${baseName}.zip`;
|
|
744
|
+
const filePath = node_path_1.default.join(outputDir, fileName);
|
|
745
|
+
try {
|
|
746
|
+
await node_fs_1.default.promises.access(filePath);
|
|
747
|
+
// File exists, append timestamp
|
|
748
|
+
return `${baseName}_${Date.now()}.zip`;
|
|
749
|
+
}
|
|
750
|
+
catch {
|
|
751
|
+
// File doesn't exist, use base name
|
|
752
|
+
return fileName;
|
|
704
753
|
}
|
|
705
|
-
// Generate unique name with timestamp
|
|
706
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
707
|
-
return `maestro_artifacts_${timestamp}.zip`;
|
|
708
754
|
}
|
|
709
755
|
async downloadArtifacts(runs) {
|
|
710
756
|
if (!this.options.downloadArtifacts)
|
|
@@ -730,12 +776,12 @@ class Maestro {
|
|
|
730
776
|
const runDir = node_path_1.default.join(tempDir, `run_${run.id}`);
|
|
731
777
|
await node_fs_1.default.promises.mkdir(runDir, { recursive: true });
|
|
732
778
|
// Download logs
|
|
733
|
-
if (runDetails.assets.logs &&
|
|
779
|
+
if (runDetails.assets.logs &&
|
|
780
|
+
Object.keys(runDetails.assets.logs).length > 0) {
|
|
734
781
|
const logsDir = node_path_1.default.join(runDir, 'logs');
|
|
735
782
|
await node_fs_1.default.promises.mkdir(logsDir, { recursive: true });
|
|
736
|
-
for (
|
|
737
|
-
const
|
|
738
|
-
const logFileName = node_path_1.default.basename(logUrl) || `log_${i}.txt`;
|
|
783
|
+
for (const [logName, logUrl] of Object.entries(runDetails.assets.logs)) {
|
|
784
|
+
const logFileName = `${logName}.txt`;
|
|
739
785
|
const logPath = node_path_1.default.join(logsDir, logFileName);
|
|
740
786
|
try {
|
|
741
787
|
await this.downloadFile(logUrl, logPath);
|
|
@@ -753,7 +799,7 @@ class Maestro {
|
|
|
753
799
|
const videoDir = node_path_1.default.join(runDir, 'video');
|
|
754
800
|
await node_fs_1.default.promises.mkdir(videoDir, { recursive: true });
|
|
755
801
|
const videoUrl = runDetails.assets.video;
|
|
756
|
-
const videoFileName =
|
|
802
|
+
const videoFileName = 'video.mp4';
|
|
757
803
|
const videoPath = node_path_1.default.join(videoDir, videoFileName);
|
|
758
804
|
try {
|
|
759
805
|
await this.downloadFile(videoUrl, videoPath);
|
|
@@ -771,7 +817,7 @@ class Maestro {
|
|
|
771
817
|
await node_fs_1.default.promises.mkdir(screenshotsDir, { recursive: true });
|
|
772
818
|
for (let i = 0; i < runDetails.assets.screenshots.length; i++) {
|
|
773
819
|
const screenshotUrl = runDetails.assets.screenshots[i];
|
|
774
|
-
const screenshotFileName =
|
|
820
|
+
const screenshotFileName = `screenshot_${i}.png`;
|
|
775
821
|
const screenshotPath = node_path_1.default.join(screenshotsDir, screenshotFileName);
|
|
776
822
|
try {
|
|
777
823
|
await this.downloadFile(screenshotUrl, screenshotPath);
|
|
@@ -804,7 +850,7 @@ class Maestro {
|
|
|
804
850
|
logger_1.default.error(`Failed to download artifacts for run ${run.id}: ${error instanceof Error ? error.message : error}`);
|
|
805
851
|
}
|
|
806
852
|
}
|
|
807
|
-
const zipFileName = this.generateArtifactZipName();
|
|
853
|
+
const zipFileName = await this.generateArtifactZipName(outputDir);
|
|
808
854
|
const zipFilePath = node_path_1.default.join(outputDir, zipFileName);
|
|
809
855
|
if (!this.options.quiet) {
|
|
810
856
|
logger_1.default.info(`Creating artifacts zip: ${zipFileName}`);
|
package/dist/upload.d.ts
CHANGED
|
@@ -11,11 +11,8 @@ export interface UploadResult {
|
|
|
11
11
|
id: number;
|
|
12
12
|
}
|
|
13
13
|
export default class Upload {
|
|
14
|
-
private lastProgressPercent;
|
|
15
14
|
upload(options: UploadOptions): Promise<UploadResult>;
|
|
15
|
+
private drawProgressBar;
|
|
16
16
|
private validateFile;
|
|
17
|
-
private handleProgress;
|
|
18
|
-
private displayProgress;
|
|
19
|
-
private clearProgressLine;
|
|
20
17
|
}
|
|
21
18
|
//# sourceMappingURL=upload.d.ts.map
|
package/dist/upload.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AAKA,OAAO,WAAW,MAAM,sBAAsB,CAAC;AAI/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;CACxB;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;IAuGlE,OAAO,CAAC,eAAe;YAgBT,YAAY;CAO3B"}
|
package/dist/upload.js
CHANGED
|
@@ -7,23 +7,47 @@ const axios_1 = __importDefault(require("axios"));
|
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
9
|
const form_data_1 = __importDefault(require("form-data"));
|
|
10
|
+
const progress_stream_1 = __importDefault(require("progress-stream"));
|
|
10
11
|
const testingbot_error_1 = __importDefault(require("./models/testingbot_error"));
|
|
11
12
|
const utils_1 = __importDefault(require("./utils"));
|
|
12
13
|
class Upload {
|
|
13
|
-
lastProgressPercent = 0;
|
|
14
14
|
async upload(options) {
|
|
15
|
-
const { filePath, url, credentials,
|
|
15
|
+
const { filePath, url, credentials, showProgress = false, } = options;
|
|
16
16
|
await this.validateFile(filePath);
|
|
17
17
|
const fileName = node_path_1.default.basename(filePath);
|
|
18
18
|
const fileStats = await node_fs_1.default.promises.stat(filePath);
|
|
19
|
+
const totalSize = fileStats.size;
|
|
20
|
+
const sizeMB = (totalSize / (1024 * 1024)).toFixed(2);
|
|
21
|
+
// Create progress tracker
|
|
22
|
+
const progressTracker = (0, progress_stream_1.default)({
|
|
23
|
+
length: totalSize,
|
|
24
|
+
time: 100, // Emit progress every 100ms
|
|
25
|
+
});
|
|
26
|
+
let lastPercent = 0;
|
|
27
|
+
if (showProgress) {
|
|
28
|
+
// Draw initial progress bar
|
|
29
|
+
this.drawProgressBar(fileName, sizeMB, 0);
|
|
30
|
+
progressTracker.on('progress', (prog) => {
|
|
31
|
+
const percent = Math.round(prog.percentage);
|
|
32
|
+
if (percent !== lastPercent) {
|
|
33
|
+
lastPercent = percent;
|
|
34
|
+
this.drawProgressBar(fileName, sizeMB, percent);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
// Create file stream and pipe through progress tracker
|
|
19
39
|
const fileStream = node_fs_1.default.createReadStream(filePath);
|
|
40
|
+
const trackedStream = fileStream.pipe(progressTracker);
|
|
20
41
|
const formData = new form_data_1.default();
|
|
21
|
-
formData.append('file',
|
|
42
|
+
formData.append('file', trackedStream, {
|
|
43
|
+
filename: fileName,
|
|
44
|
+
contentType: options.contentType,
|
|
45
|
+
knownLength: totalSize,
|
|
46
|
+
});
|
|
22
47
|
try {
|
|
23
48
|
const response = await axios_1.default.post(url, formData, {
|
|
24
49
|
headers: {
|
|
25
|
-
|
|
26
|
-
'Content-Disposition': `attachment; filename=${fileName}`,
|
|
50
|
+
...formData.getHeaders(),
|
|
27
51
|
'User-Agent': utils_1.default.getUserAgent(),
|
|
28
52
|
},
|
|
29
53
|
auth: {
|
|
@@ -32,34 +56,52 @@ class Upload {
|
|
|
32
56
|
},
|
|
33
57
|
maxContentLength: Infinity,
|
|
34
58
|
maxBodyLength: Infinity,
|
|
35
|
-
|
|
36
|
-
? (progressEvent) => {
|
|
37
|
-
this.handleProgress(progressEvent, fileStats.size, fileName);
|
|
38
|
-
}
|
|
39
|
-
: undefined,
|
|
59
|
+
maxRedirects: 0, // Recommended for stream uploads to avoid buffering
|
|
40
60
|
});
|
|
41
61
|
const result = response.data;
|
|
42
62
|
if (result.id) {
|
|
43
63
|
if (showProgress) {
|
|
44
|
-
this.
|
|
64
|
+
this.drawProgressBar(fileName, sizeMB, 100);
|
|
65
|
+
console.log('');
|
|
45
66
|
}
|
|
46
67
|
return { id: result.id };
|
|
47
68
|
}
|
|
48
69
|
else {
|
|
70
|
+
if (showProgress) {
|
|
71
|
+
console.log(' Failed');
|
|
72
|
+
}
|
|
49
73
|
throw new testingbot_error_1.default(`Upload failed: ${result.error || 'Unknown error'}`);
|
|
50
74
|
}
|
|
51
75
|
}
|
|
52
76
|
catch (error) {
|
|
77
|
+
if (showProgress) {
|
|
78
|
+
console.log(' Failed');
|
|
79
|
+
}
|
|
53
80
|
if (error instanceof testingbot_error_1.default) {
|
|
54
81
|
throw error;
|
|
55
82
|
}
|
|
56
83
|
if (axios_1.default.isAxiosError(error)) {
|
|
84
|
+
if (error.response?.status === 401) {
|
|
85
|
+
throw new testingbot_error_1.default('Invalid TestingBot credentials. Please check your API key and secret.\n' +
|
|
86
|
+
'You can update your credentials by running "testingbot login" or by using:\n' +
|
|
87
|
+
' --api-key and --api-secret options\n' +
|
|
88
|
+
' TB_KEY and TB_SECRET environment variables\n' +
|
|
89
|
+
' ~/.testingbot file with content: key:secret');
|
|
90
|
+
}
|
|
57
91
|
const message = error.response?.data?.error || error.message;
|
|
58
92
|
throw new testingbot_error_1.default(`Upload failed: ${message}`);
|
|
59
93
|
}
|
|
60
94
|
throw new testingbot_error_1.default(`Upload failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
61
95
|
}
|
|
62
96
|
}
|
|
97
|
+
drawProgressBar(fileName, sizeMB, percent) {
|
|
98
|
+
const barWidth = 30;
|
|
99
|
+
const filled = Math.round((barWidth * percent) / 100);
|
|
100
|
+
const empty = barWidth - filled;
|
|
101
|
+
const bar = '█'.repeat(filled) + '░'.repeat(empty);
|
|
102
|
+
const transferred = ((percent / 100) * parseFloat(sizeMB)).toFixed(2);
|
|
103
|
+
process.stdout.write(`\r ${fileName}: [${bar}] ${percent}% (${transferred}/${sizeMB} MB)`);
|
|
104
|
+
}
|
|
63
105
|
async validateFile(filePath) {
|
|
64
106
|
try {
|
|
65
107
|
await node_fs_1.default.promises.access(filePath, node_fs_1.default.constants.R_OK);
|
|
@@ -68,27 +110,5 @@ class Upload {
|
|
|
68
110
|
throw new testingbot_error_1.default(`File not found or not readable: ${filePath}`);
|
|
69
111
|
}
|
|
70
112
|
}
|
|
71
|
-
handleProgress(progressEvent, totalSize, fileName) {
|
|
72
|
-
const loaded = progressEvent.loaded;
|
|
73
|
-
const total = progressEvent.total || totalSize;
|
|
74
|
-
const percent = Math.round((loaded / total) * 100);
|
|
75
|
-
if (percent !== this.lastProgressPercent) {
|
|
76
|
-
this.lastProgressPercent = percent;
|
|
77
|
-
this.displayProgress(fileName, percent, loaded, total);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
displayProgress(fileName, percent, loaded, total) {
|
|
81
|
-
const barWidth = 30;
|
|
82
|
-
const filledWidth = Math.round((percent / 100) * barWidth);
|
|
83
|
-
const emptyWidth = barWidth - filledWidth;
|
|
84
|
-
const bar = '█'.repeat(filledWidth) + '░'.repeat(emptyWidth);
|
|
85
|
-
const loadedMB = (loaded / (1024 * 1024)).toFixed(2);
|
|
86
|
-
const totalMB = (total / (1024 * 1024)).toFixed(2);
|
|
87
|
-
process.stdout.write(`\r ${fileName}: [${bar}] ${percent}% (${loadedMB}/${totalMB} MB)`);
|
|
88
|
-
}
|
|
89
|
-
clearProgressLine() {
|
|
90
|
-
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
91
|
-
this.lastProgressPercent = 0;
|
|
92
|
-
}
|
|
93
113
|
}
|
|
94
114
|
exports.default = Upload;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testingbot/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
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": {
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"form-data": "^4.0.5",
|
|
56
56
|
"glob": "^13.0.0",
|
|
57
57
|
"js-yaml": "^4.1.1",
|
|
58
|
+
"progress-stream": "^2.0.0",
|
|
58
59
|
"socket.io-client": "^4.8.1",
|
|
59
60
|
"tracer": "^1.3.0"
|
|
60
61
|
},
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
"@types/jest": "^29.5.14",
|
|
66
67
|
"@types/js-yaml": "^4.0.9",
|
|
67
68
|
"@types/node": "^20.19.0",
|
|
69
|
+
"@types/progress-stream": "^2.0.5",
|
|
68
70
|
"babel-jest": "^29.7.0",
|
|
69
71
|
"eslint": "^9.39.1",
|
|
70
72
|
"eslint-config-prettier": "^10.1.8",
|