@testingbot/cli 1.0.6 → 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 +79 -50
- 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 +11 -4
- package/dist/models/espresso_options.d.ts.map +1 -1
- package/dist/models/espresso_options.js +24 -7
- package/dist/models/maestro_options.d.ts +13 -3
- package/dist/models/maestro_options.d.ts.map +1 -1
- package/dist/models/maestro_options.js +25 -2
- package/dist/models/xcuitest_options.d.ts +11 -4
- package/dist/models/xcuitest_options.d.ts.map +1 -1
- package/dist/models/xcuitest_options.js +24 -7
- 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 +21 -0
- package/dist/providers/maestro.d.ts.map +1 -1
- package/dist/providers/maestro.js +320 -72
- 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
|
@@ -36,6 +36,7 @@ export default class XCUITest extends BaseProvider<XCUITestOptions> {
|
|
|
36
36
|
private socket;
|
|
37
37
|
private updateServer;
|
|
38
38
|
private updateKey;
|
|
39
|
+
private socketFallbackWarned;
|
|
39
40
|
constructor(credentials: Credentials, options: XCUITestOptions);
|
|
40
41
|
private validate;
|
|
41
42
|
run(): Promise<XCUITestResult>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"xcuitest.d.ts","sourceRoot":"","sources":["../../src/providers/xcuitest.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,MAAM,4BAA4B,CAAC;AAEzD,OAAO,WAAW,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"xcuitest.d.ts","sourceRoot":"","sources":["../../src/providers/xcuitest.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,MAAM,4BAA4B,CAAC;AAEzD,OAAO,WAAW,MAAM,uBAAuB,CAAC;AAQhD,OAAO,YAAY,MAAM,iBAAiB,CAAC;AAI3C,MAAM,WAAW,sBAAsB;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;IAChD,YAAY,EAAE;QACZ,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;QACrB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,WAAW,CAAC,EAAE,sBAAsB,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,eAAe,EAAE,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,eAAe,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,CAAC,OAAO,OAAO,QAAS,SAAQ,YAAY,CAAC,eAAe,CAAC;IACjE,SAAS,CAAC,QAAQ,CAAC,GAAG,yDACkC;IAExD,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,oBAAoB,CAAS;gBAElB,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,eAAe;YAIvD,QAAQ;IAuCT,GAAG,IAAI,OAAO,CAAC,cAAc,CAAC;YA6G7B,SAAS;YAcT,aAAa;YAab,QAAQ;YA0DR,SAAS;YA4BT,iBAAiB;IA8F/B,OAAO,CAAC,gBAAgB;IAwCxB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,aAAa;YAkBP,YAAY;IA2D1B,OAAO,CAAC,qBAAqB;IA6C7B,OAAO,CAAC,0BAA0B;IAOlC,OAAO,CAAC,kBAAkB;IAc1B,OAAO,CAAC,mBAAmB;CAa5B"}
|
|
@@ -7,15 +7,19 @@ const logger_1 = __importDefault(require("../logger"));
|
|
|
7
7
|
const axios_1 = __importDefault(require("axios"));
|
|
8
8
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
10
11
|
const socket_io_client_1 = require("socket.io-client");
|
|
11
12
|
const testingbot_error_1 = __importDefault(require("../models/testingbot_error"));
|
|
12
13
|
const utils_1 = __importDefault(require("../utils"));
|
|
13
14
|
const base_provider_1 = __importDefault(require("./base_provider"));
|
|
15
|
+
const terminal_title_1 = require("../ui/terminal-title");
|
|
16
|
+
const constants_1 = require("../config/constants");
|
|
14
17
|
class XCUITest extends base_provider_1.default {
|
|
15
18
|
URL = 'https://api.testingbot.com/v1/app-automate/xcuitest';
|
|
16
19
|
socket = null;
|
|
17
20
|
updateServer = null;
|
|
18
21
|
updateKey = null;
|
|
22
|
+
socketFallbackWarned = false;
|
|
19
23
|
constructor(credentials, options) {
|
|
20
24
|
super(credentials, options);
|
|
21
25
|
}
|
|
@@ -79,17 +83,25 @@ class XCUITest extends base_provider_1.default {
|
|
|
79
83
|
try {
|
|
80
84
|
// Quick connectivity check before starting uploads
|
|
81
85
|
await this.ensureConnectivity();
|
|
86
|
+
(0, terminal_title_1.setTitle)('xcuitest');
|
|
82
87
|
if (!this.options.quiet) {
|
|
83
88
|
logger_1.default.info('Uploading XCUITest App');
|
|
84
89
|
}
|
|
90
|
+
(0, terminal_title_1.setTitle)('xcuitest · uploading app');
|
|
85
91
|
await this.uploadApp();
|
|
86
92
|
if (!this.options.quiet) {
|
|
87
93
|
logger_1.default.info('Uploading XCUITest Test App');
|
|
88
94
|
}
|
|
95
|
+
(0, terminal_title_1.setTitle)('xcuitest · uploading test app');
|
|
89
96
|
await this.uploadTestApp();
|
|
97
|
+
if (this.options.tunnel && this.options.async) {
|
|
98
|
+
throw new testingbot_error_1.default('Cannot use --tunnel with --async mode. The tunnel would close when the CLI exits. Use a standalone tunnel instead.');
|
|
99
|
+
}
|
|
100
|
+
await this.startTunnel();
|
|
90
101
|
if (!this.options.quiet) {
|
|
91
102
|
logger_1.default.info('Running XCUITests');
|
|
92
103
|
}
|
|
104
|
+
(0, terminal_title_1.setTitle)('xcuitest · queued');
|
|
93
105
|
await this.runTests();
|
|
94
106
|
if (this.options.async) {
|
|
95
107
|
if (!this.options.quiet) {
|
|
@@ -108,12 +120,16 @@ class XCUITest extends base_provider_1.default {
|
|
|
108
120
|
// Clean up
|
|
109
121
|
this.disconnectFromUpdateServer();
|
|
110
122
|
this.removeSignalHandlers();
|
|
123
|
+
await this.stopTunnel();
|
|
111
124
|
return result;
|
|
112
125
|
}
|
|
113
126
|
catch (error) {
|
|
114
127
|
// Clean up on error
|
|
128
|
+
this.spinner.stop();
|
|
115
129
|
this.disconnectFromUpdateServer();
|
|
116
130
|
this.removeSignalHandlers();
|
|
131
|
+
await this.stopTunnel();
|
|
132
|
+
(0, terminal_title_1.setTitle)('xcuitest · ✘ error');
|
|
117
133
|
logger_1.default.error(error instanceof Error ? error.message : error);
|
|
118
134
|
if (error instanceof Error && error.cause) {
|
|
119
135
|
const causeMessage = this.extractErrorMessage(error.cause);
|
|
@@ -165,7 +181,7 @@ class XCUITest extends base_provider_1.default {
|
|
|
165
181
|
username: this.credentials.userName,
|
|
166
182
|
password: this.credentials.accessKey,
|
|
167
183
|
},
|
|
168
|
-
timeout:
|
|
184
|
+
timeout: constants_1.HTTP.TIMEOUT_MS,
|
|
169
185
|
});
|
|
170
186
|
// Check for version update notification
|
|
171
187
|
const latestVersion = response.headers?.['x-testingbotctl-version'];
|
|
@@ -202,7 +218,7 @@ class XCUITest extends base_provider_1.default {
|
|
|
202
218
|
username: this.credentials.userName,
|
|
203
219
|
password: this.credentials.accessKey,
|
|
204
220
|
},
|
|
205
|
-
timeout:
|
|
221
|
+
timeout: constants_1.HTTP.TIMEOUT_MS,
|
|
206
222
|
});
|
|
207
223
|
// Check for version update notification
|
|
208
224
|
const latestVersion = response.headers?.['x-testingbotctl-version'];
|
|
@@ -215,10 +231,11 @@ class XCUITest extends base_provider_1.default {
|
|
|
215
231
|
}
|
|
216
232
|
}
|
|
217
233
|
async waitForCompletion() {
|
|
218
|
-
let attempts = 0;
|
|
219
234
|
const startTime = Date.now();
|
|
220
235
|
const previousStatus = new Map();
|
|
221
|
-
|
|
236
|
+
let pollInterval = this.MIN_POLL_INTERVAL_MS;
|
|
237
|
+
let previousSignature = null;
|
|
238
|
+
while (true) {
|
|
222
239
|
if (this.isShuttingDown) {
|
|
223
240
|
throw new testingbot_error_1.default('Test run cancelled by user');
|
|
224
241
|
}
|
|
@@ -231,24 +248,34 @@ class XCUITest extends base_provider_1.default {
|
|
|
231
248
|
if (!this.options.quiet) {
|
|
232
249
|
this.displayRunStatus(status.runs, startTime, previousStatus);
|
|
233
250
|
}
|
|
251
|
+
const running = status.runs.find((r) => r.status === 'READY');
|
|
252
|
+
if (running) {
|
|
253
|
+
const device = running.environment?.name || running.capabilities.deviceName;
|
|
254
|
+
(0, terminal_title_1.setTitle)(`xcuitest · running · ${device}`);
|
|
255
|
+
}
|
|
234
256
|
if (status.completed) {
|
|
235
|
-
//
|
|
257
|
+
// Stop the spinner and print final status
|
|
236
258
|
if (!this.options.quiet) {
|
|
237
|
-
this.
|
|
259
|
+
this.spinner.stop();
|
|
238
260
|
for (const run of status.runs) {
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
|
|
261
|
+
const passed = run.success === 1;
|
|
262
|
+
const symbol = passed ? picocolors_1.default.green('✔') : picocolors_1.default.red('✘');
|
|
263
|
+
const statusText = passed
|
|
264
|
+
? picocolors_1.default.green('Test completed successfully')
|
|
265
|
+
: picocolors_1.default.red('Test failed');
|
|
266
|
+
console.log(` ${symbol} Run ${run.id} ${picocolors_1.default.dim(`(${this.getRunDisplayName(run)})`)}: ${statusText}`);
|
|
242
267
|
}
|
|
243
268
|
}
|
|
244
269
|
const allSucceeded = status.runs.every((run) => run.success === 1);
|
|
245
270
|
if (allSucceeded) {
|
|
271
|
+
(0, terminal_title_1.setTitle)('xcuitest · ✔ passed');
|
|
246
272
|
if (!this.options.quiet) {
|
|
247
273
|
logger_1.default.info('All tests completed successfully!');
|
|
248
274
|
}
|
|
249
275
|
}
|
|
250
276
|
else {
|
|
251
277
|
const failedRuns = status.runs.filter((run) => run.success !== 1);
|
|
278
|
+
(0, terminal_title_1.setTitle)(`xcuitest · ✘ ${failedRuns.length} failed`);
|
|
252
279
|
logger_1.default.error(`${failedRuns.length} test run(s) failed:`);
|
|
253
280
|
for (const run of failedRuns) {
|
|
254
281
|
logger_1.default.error(` - Run ${run.id} (${this.getRunDisplayName(run)}): ${run.report || 'No report available'}`);
|
|
@@ -263,32 +290,45 @@ class XCUITest extends base_provider_1.default {
|
|
|
263
290
|
runs: status.runs,
|
|
264
291
|
};
|
|
265
292
|
}
|
|
266
|
-
|
|
267
|
-
|
|
293
|
+
// Checked after getStatus() so a run that completes during the final
|
|
294
|
+
// sleep is returned as success on the next iteration instead of being
|
|
295
|
+
// misreported as a timeout.
|
|
296
|
+
if (Date.now() - startTime >= this.MAX_POLL_DURATION_MS) {
|
|
297
|
+
throw new testingbot_error_1.default(`Test timed out after ${this.MAX_POLL_DURATION_MS / 1000 / 60} minutes`);
|
|
298
|
+
}
|
|
299
|
+
const signature = JSON.stringify(status.runs.map((r) => [r.id, r.status, r.success]));
|
|
300
|
+
const changed = signature !== previousSignature;
|
|
301
|
+
previousSignature = signature;
|
|
302
|
+
pollInterval = this.computeNextPollInterval(pollInterval, changed);
|
|
303
|
+
await this.sleep(pollInterval);
|
|
268
304
|
}
|
|
269
|
-
throw new testingbot_error_1.default(`Test timed out after ${(this.MAX_POLL_ATTEMPTS * this.POLL_INTERVAL_MS) / 1000 / 60} minutes`);
|
|
270
305
|
}
|
|
271
306
|
displayRunStatus(runs, startTime, previousStatus) {
|
|
272
307
|
const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
|
|
273
308
|
const elapsedStr = this.formatElapsedTime(elapsedSeconds);
|
|
309
|
+
const activeMessages = [];
|
|
274
310
|
for (const run of runs) {
|
|
275
311
|
const prevStatus = previousStatus.get(run.id);
|
|
276
312
|
const statusChanged = prevStatus !== run.status;
|
|
277
|
-
if (statusChanged &&
|
|
278
|
-
prevStatus &&
|
|
279
|
-
(prevStatus === 'WAITING' || prevStatus === 'READY')) {
|
|
280
|
-
this.clearLine();
|
|
281
|
-
}
|
|
282
313
|
previousStatus.set(run.id, run.status);
|
|
283
314
|
const statusInfo = this.getStatusInfo(run.status);
|
|
284
315
|
if (run.status === 'WAITING' || run.status === 'READY') {
|
|
285
|
-
const
|
|
286
|
-
|
|
316
|
+
const label = run.status === 'WAITING'
|
|
317
|
+
? picocolors_1.default.yellow(statusInfo.text)
|
|
318
|
+
: picocolors_1.default.cyan(statusInfo.text);
|
|
319
|
+
activeMessages.push(`${label} ${picocolors_1.default.dim(`• Run ${run.id} (${this.getRunDisplayName(run)}) • ${elapsedStr}`)}`);
|
|
287
320
|
}
|
|
288
321
|
else if (statusChanged) {
|
|
289
|
-
|
|
322
|
+
this.spinner.clearLine();
|
|
323
|
+
console.log(` ${statusInfo.symbol} Run ${run.id} ${picocolors_1.default.dim(`(${this.getRunDisplayName(run)})`)}: ${statusInfo.text}`);
|
|
290
324
|
}
|
|
291
325
|
}
|
|
326
|
+
if (activeMessages.length > 0) {
|
|
327
|
+
this.spinner.setMessage(activeMessages.join(picocolors_1.default.dim(' ┊ ')));
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
this.spinner.stop();
|
|
331
|
+
}
|
|
292
332
|
}
|
|
293
333
|
/**
|
|
294
334
|
* Get the display name for a run, preferring environment.name over capabilities.deviceName
|
|
@@ -300,15 +340,15 @@ class XCUITest extends base_provider_1.default {
|
|
|
300
340
|
getStatusInfo(status) {
|
|
301
341
|
switch (status) {
|
|
302
342
|
case 'WAITING':
|
|
303
|
-
return {
|
|
343
|
+
return { symbol: picocolors_1.default.yellow('◐'), text: 'Waiting for test to start' };
|
|
304
344
|
case 'READY':
|
|
305
|
-
return {
|
|
345
|
+
return { symbol: picocolors_1.default.cyan('◑'), text: 'Running test' };
|
|
306
346
|
case 'DONE':
|
|
307
|
-
return {
|
|
347
|
+
return { symbol: picocolors_1.default.green('✔'), text: 'Test has finished running' };
|
|
308
348
|
case 'FAILED':
|
|
309
|
-
return {
|
|
349
|
+
return { symbol: picocolors_1.default.red('✘'), text: 'Test failed' };
|
|
310
350
|
default:
|
|
311
|
-
return {
|
|
351
|
+
return { symbol: picocolors_1.default.dim('?'), text: status };
|
|
312
352
|
}
|
|
313
353
|
}
|
|
314
354
|
async fetchReports(runs) {
|
|
@@ -334,7 +374,7 @@ class XCUITest extends base_provider_1.default {
|
|
|
334
374
|
password: this.credentials.accessKey,
|
|
335
375
|
},
|
|
336
376
|
responseType: reportFormat === 'html' ? 'arraybuffer' : 'text',
|
|
337
|
-
timeout:
|
|
377
|
+
timeout: constants_1.HTTP.TIMEOUT_MS,
|
|
338
378
|
});
|
|
339
379
|
// Check for version update notification
|
|
340
380
|
const latestVersion = response.headers?.['x-testingbotctl-version'];
|
|
@@ -365,9 +405,9 @@ class XCUITest extends base_provider_1.default {
|
|
|
365
405
|
this.socket = (0, socket_io_client_1.io)(this.updateServer, {
|
|
366
406
|
transports: ['websocket'],
|
|
367
407
|
reconnection: true,
|
|
368
|
-
reconnectionAttempts:
|
|
369
|
-
reconnectionDelay:
|
|
370
|
-
timeout:
|
|
408
|
+
reconnectionAttempts: constants_1.SOCKET.RECONNECTION_ATTEMPTS,
|
|
409
|
+
reconnectionDelay: constants_1.SOCKET.RECONNECTION_DELAY_MS,
|
|
410
|
+
timeout: constants_1.SOCKET.TIMEOUT_MS,
|
|
371
411
|
});
|
|
372
412
|
this.socket.on('connect', () => {
|
|
373
413
|
// Join the room for this test run
|
|
@@ -379,8 +419,12 @@ class XCUITest extends base_provider_1.default {
|
|
|
379
419
|
this.socket.on('xcuitest_error', (data) => {
|
|
380
420
|
this.handleXCUITestError(data);
|
|
381
421
|
});
|
|
382
|
-
this.socket.on('connect_error', () => {
|
|
383
|
-
|
|
422
|
+
this.socket.on('connect_error', (err) => {
|
|
423
|
+
if (!this.socketFallbackWarned) {
|
|
424
|
+
this.socketFallbackWarned = true;
|
|
425
|
+
logger_1.default.warn('Real-time log stream unavailable, falling back to polling.');
|
|
426
|
+
logger_1.default.debug(`Socket connect_error: ${err?.message ?? 'unknown error'}`);
|
|
427
|
+
}
|
|
384
428
|
this.disconnectFromUpdateServer();
|
|
385
429
|
});
|
|
386
430
|
}
|
|
@@ -399,8 +443,8 @@ class XCUITest extends base_provider_1.default {
|
|
|
399
443
|
try {
|
|
400
444
|
const message = JSON.parse(data);
|
|
401
445
|
if (message.payload) {
|
|
402
|
-
// Clear the
|
|
403
|
-
this.clearLine();
|
|
446
|
+
// Clear the spinner line before printing output
|
|
447
|
+
this.spinner.clearLine();
|
|
404
448
|
// Print the XCUITest output
|
|
405
449
|
process.stdout.write(message.payload);
|
|
406
450
|
}
|
|
@@ -413,8 +457,8 @@ class XCUITest extends base_provider_1.default {
|
|
|
413
457
|
try {
|
|
414
458
|
const message = JSON.parse(data);
|
|
415
459
|
if (message.payload) {
|
|
416
|
-
// Clear the
|
|
417
|
-
this.clearLine();
|
|
460
|
+
// Clear the spinner line before printing error
|
|
461
|
+
this.spinner.clearLine();
|
|
418
462
|
// Print the error output
|
|
419
463
|
process.stderr.write(message.payload);
|
|
420
464
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"banner.d.ts","sourceRoot":"","sources":["../../src/ui/banner.ts"],"names":[],"mappings":"AAsDA,wBAAgB,WAAW,IAAI,IAAI,CA8BlC;AAED,eAAe,WAAW,CAAC"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.printBanner = printBanner;
|
|
7
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
8
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
9
|
+
const utils_1 = __importDefault(require("../utils"));
|
|
10
|
+
const package_json_1 = __importDefault(require("../../package.json"));
|
|
11
|
+
// Hand-crafted 3-row block letters for TESTINGBOT using half-block
|
|
12
|
+
// characters (▀ ▄ █) so each text row encodes two pixel rows — same
|
|
13
|
+
// readability as a 5-row font, half the vertical space.
|
|
14
|
+
const LETTERS = {
|
|
15
|
+
T: ['▀█▀', ' █ ', ' ▀ '],
|
|
16
|
+
E: ['█▀▀', '█▀ ', '▀▀▀'],
|
|
17
|
+
S: ['▄▀▀▀', ' ▀▀▄', '▀▀▀ '],
|
|
18
|
+
I: ['▀█▀', ' █ ', '▀▀▀'],
|
|
19
|
+
N: ['█▄ █', '█ ▀█', '▀ ▀'],
|
|
20
|
+
G: ['▄▀▀▀', '█ ▀█', ' ▀▀▀'],
|
|
21
|
+
B: ['█▀▀▄', '█▀▀▄', '▀▀▀ '],
|
|
22
|
+
O: ['█▀▀█', '█ █', '▀▀▀▀'],
|
|
23
|
+
};
|
|
24
|
+
const WORD = 'TESTINGBOT';
|
|
25
|
+
const LETTER_GAP = ' ';
|
|
26
|
+
const LETTER_ROWS = 3;
|
|
27
|
+
function buildBigText() {
|
|
28
|
+
const rows = [];
|
|
29
|
+
for (let r = 0; r < LETTER_ROWS; r++) {
|
|
30
|
+
const parts = WORD.split('').map((ch) => LETTERS[ch][r]);
|
|
31
|
+
rows.push(parts.join(LETTER_GAP));
|
|
32
|
+
}
|
|
33
|
+
return rows;
|
|
34
|
+
}
|
|
35
|
+
function visibleLen(s) {
|
|
36
|
+
return Array.from(s).length;
|
|
37
|
+
}
|
|
38
|
+
function shouldSkipBanner(argv) {
|
|
39
|
+
if (!utils_1.default.isInteractive())
|
|
40
|
+
return true;
|
|
41
|
+
for (const arg of argv) {
|
|
42
|
+
if (arg === '--help' ||
|
|
43
|
+
arg === '-h' ||
|
|
44
|
+
arg === '--version' ||
|
|
45
|
+
arg === '-v' ||
|
|
46
|
+
arg === '-V' ||
|
|
47
|
+
arg === '--quiet' ||
|
|
48
|
+
arg === '-q') {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
function printBanner() {
|
|
55
|
+
if (shouldSkipBanner(process.argv.slice(2)))
|
|
56
|
+
return;
|
|
57
|
+
const home = node_os_1.default.homedir();
|
|
58
|
+
const rawCwd = process.cwd();
|
|
59
|
+
const cwd = rawCwd.startsWith(home)
|
|
60
|
+
? '~' + rawCwd.slice(home.length)
|
|
61
|
+
: rawCwd;
|
|
62
|
+
const version = `v${package_json_1.default.version}`;
|
|
63
|
+
const bigText = buildBigText();
|
|
64
|
+
const textWidth = Math.max(...bigText.map((l) => visibleLen(l)));
|
|
65
|
+
const infoLines = [
|
|
66
|
+
`${picocolors_1.default.dim('Espresso · XCUITest · Maestro')} ${picocolors_1.default.dim('·')} ${picocolors_1.default.dim(version)}`,
|
|
67
|
+
picocolors_1.default.dim(cwd),
|
|
68
|
+
];
|
|
69
|
+
console.log();
|
|
70
|
+
for (const line of bigText) {
|
|
71
|
+
console.log(' ' + picocolors_1.default.blue(line));
|
|
72
|
+
}
|
|
73
|
+
// Info lines sit flush-left under the big text, roughly aligned with its
|
|
74
|
+
// left edge, so both read as one block.
|
|
75
|
+
void textWidth;
|
|
76
|
+
console.log();
|
|
77
|
+
for (const line of infoLines) {
|
|
78
|
+
console.log(' ' + line);
|
|
79
|
+
}
|
|
80
|
+
console.log();
|
|
81
|
+
}
|
|
82
|
+
exports.default = printBanner;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-line animated spinner for long-running status updates. Writes
|
|
3
|
+
* `\r{frame} {message}` on each tick to rewrite the current line in place.
|
|
4
|
+
*
|
|
5
|
+
* In non-interactive environments (no TTY, or CI=1) the spinner is a no-op —
|
|
6
|
+
* providers already `console.log` status transitions, so CI logs stay readable.
|
|
7
|
+
*/
|
|
8
|
+
export default class Spinner {
|
|
9
|
+
private frame;
|
|
10
|
+
private timer;
|
|
11
|
+
private message;
|
|
12
|
+
private active;
|
|
13
|
+
private readonly interactive;
|
|
14
|
+
constructor();
|
|
15
|
+
/** Starts (or refreshes) the spinner with the given message. */
|
|
16
|
+
start(message: string): void;
|
|
17
|
+
/** Updates the visible message; starts the spinner if it wasn't running. */
|
|
18
|
+
setMessage(message: string): void;
|
|
19
|
+
/**
|
|
20
|
+
* Clears the current spinner line without stopping the timer. Call before
|
|
21
|
+
* printing unrelated output that shouldn't be overwritten by the next tick.
|
|
22
|
+
*/
|
|
23
|
+
clearLine(): void;
|
|
24
|
+
/**
|
|
25
|
+
* Stops the animation, clears the line, and optionally prints a final
|
|
26
|
+
* non-animated line in its place.
|
|
27
|
+
*/
|
|
28
|
+
stop(finalLine?: string): void;
|
|
29
|
+
isActive(): boolean;
|
|
30
|
+
private render;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=spinner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spinner.d.ts","sourceRoot":"","sources":["../../src/ui/spinner.ts"],"names":[],"mappings":"AAOA;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,OAAO,OAAO;IAC1B,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,OAAO,CAAM;IACrB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAU;;IAMtC,gEAAgE;IACzD,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAiBnC,4EAA4E;IACrE,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAUxC;;;OAGG;IACI,SAAS,IAAI,IAAI;IAKxB;;;OAGG;IACI,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAc9B,QAAQ,IAAI,OAAO;IAI1B,OAAO,CAAC,MAAM;CAKf"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
7
|
+
const utils_1 = __importDefault(require("../utils"));
|
|
8
|
+
const platform_1 = __importDefault(require("../utils/platform"));
|
|
9
|
+
const BRAILLE_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
10
|
+
const TICK_MS = 80;
|
|
11
|
+
/**
|
|
12
|
+
* Single-line animated spinner for long-running status updates. Writes
|
|
13
|
+
* `\r{frame} {message}` on each tick to rewrite the current line in place.
|
|
14
|
+
*
|
|
15
|
+
* In non-interactive environments (no TTY, or CI=1) the spinner is a no-op —
|
|
16
|
+
* providers already `console.log` status transitions, so CI logs stay readable.
|
|
17
|
+
*/
|
|
18
|
+
class Spinner {
|
|
19
|
+
frame = 0;
|
|
20
|
+
timer = null;
|
|
21
|
+
message = '';
|
|
22
|
+
active = false;
|
|
23
|
+
interactive;
|
|
24
|
+
constructor() {
|
|
25
|
+
this.interactive = utils_1.default.isInteractive();
|
|
26
|
+
}
|
|
27
|
+
/** Starts (or refreshes) the spinner with the given message. */
|
|
28
|
+
start(message) {
|
|
29
|
+
this.message = message;
|
|
30
|
+
if (!this.interactive)
|
|
31
|
+
return;
|
|
32
|
+
if (this.active) {
|
|
33
|
+
this.render();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
this.active = true;
|
|
37
|
+
this.render();
|
|
38
|
+
this.timer = setInterval(() => {
|
|
39
|
+
this.frame = (this.frame + 1) % BRAILLE_FRAMES.length;
|
|
40
|
+
this.render();
|
|
41
|
+
}, TICK_MS);
|
|
42
|
+
// Don't keep the event loop alive on this timer.
|
|
43
|
+
this.timer.unref?.();
|
|
44
|
+
}
|
|
45
|
+
/** Updates the visible message; starts the spinner if it wasn't running. */
|
|
46
|
+
setMessage(message) {
|
|
47
|
+
if (!this.active) {
|
|
48
|
+
this.start(message);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (this.message === message)
|
|
52
|
+
return;
|
|
53
|
+
this.message = message;
|
|
54
|
+
this.render();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Clears the current spinner line without stopping the timer. Call before
|
|
58
|
+
* printing unrelated output that shouldn't be overwritten by the next tick.
|
|
59
|
+
*/
|
|
60
|
+
clearLine() {
|
|
61
|
+
if (!this.interactive || !this.active)
|
|
62
|
+
return;
|
|
63
|
+
platform_1.default.clearLine();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Stops the animation, clears the line, and optionally prints a final
|
|
67
|
+
* non-animated line in its place.
|
|
68
|
+
*/
|
|
69
|
+
stop(finalLine) {
|
|
70
|
+
if (this.timer) {
|
|
71
|
+
clearInterval(this.timer);
|
|
72
|
+
this.timer = null;
|
|
73
|
+
}
|
|
74
|
+
if (this.active && this.interactive) {
|
|
75
|
+
platform_1.default.clearLine();
|
|
76
|
+
}
|
|
77
|
+
this.active = false;
|
|
78
|
+
if (finalLine !== undefined) {
|
|
79
|
+
console.log(finalLine);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
isActive() {
|
|
83
|
+
return this.active;
|
|
84
|
+
}
|
|
85
|
+
render() {
|
|
86
|
+
if (!this.interactive)
|
|
87
|
+
return;
|
|
88
|
+
const frame = picocolors_1.default.cyan(BRAILLE_FRAMES[this.frame]);
|
|
89
|
+
process.stdout.write(`\r${frame} ${this.message}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
exports.default = Spinner;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function setTitle(title: string): void;
|
|
2
|
+
export declare function resetTitle(): void;
|
|
3
|
+
declare const _default: {
|
|
4
|
+
setTitle: typeof setTitle;
|
|
5
|
+
resetTitle: typeof resetTitle;
|
|
6
|
+
};
|
|
7
|
+
export default _default;
|
|
8
|
+
//# sourceMappingURL=terminal-title.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal-title.d.ts","sourceRoot":"","sources":["../../src/ui/terminal-title.ts"],"names":[],"mappings":"AAsCA,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAO5C;AAED,wBAAgB,UAAU,IAAI,IAAI,CAIjC;;;;;AAED,wBAAwC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.setTitle = setTitle;
|
|
7
|
+
exports.resetTitle = resetTitle;
|
|
8
|
+
const utils_1 = __importDefault(require("../utils"));
|
|
9
|
+
/**
|
|
10
|
+
* Updates the terminal tab/window title via the OSC 2 escape sequence.
|
|
11
|
+
* No-op in CI or when stdout isn't a TTY — CI logs shouldn't carry control
|
|
12
|
+
* sequences, and non-interactive consumers wouldn't see the effect anyway.
|
|
13
|
+
*
|
|
14
|
+
* An exit handler (installed lazily) resets the title so the user's shell
|
|
15
|
+
* doesn't keep our last status after the CLI exits.
|
|
16
|
+
*/
|
|
17
|
+
const RESET_TITLE = 'Terminal';
|
|
18
|
+
let exitHookInstalled = false;
|
|
19
|
+
let lastSetTitle = null;
|
|
20
|
+
function canWriteTitle() {
|
|
21
|
+
return utils_1.default.isInteractive();
|
|
22
|
+
}
|
|
23
|
+
function writeOsc(title) {
|
|
24
|
+
// OSC 0 sets both the icon/tab title AND the window title. iTerm2 shows
|
|
25
|
+
// the session name in the tab and ignores OSC 2 (window-title-only), so
|
|
26
|
+
// OSC 0 is the portable choice across Terminal.app, iTerm2, most Linux
|
|
27
|
+
// terminals, and Windows Terminal.
|
|
28
|
+
process.stdout.write(`\x1b]0;${title}\x07`);
|
|
29
|
+
}
|
|
30
|
+
function installExitHook() {
|
|
31
|
+
if (exitHookInstalled)
|
|
32
|
+
return;
|
|
33
|
+
exitHookInstalled = true;
|
|
34
|
+
const reset = () => {
|
|
35
|
+
if (canWriteTitle() && lastSetTitle !== null) {
|
|
36
|
+
writeOsc(RESET_TITLE);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
process.on('exit', reset);
|
|
40
|
+
}
|
|
41
|
+
function setTitle(title) {
|
|
42
|
+
if (!canWriteTitle())
|
|
43
|
+
return;
|
|
44
|
+
const full = `testingbot · ${title}`;
|
|
45
|
+
if (full === lastSetTitle)
|
|
46
|
+
return;
|
|
47
|
+
lastSetTitle = full;
|
|
48
|
+
writeOsc(full);
|
|
49
|
+
installExitHook();
|
|
50
|
+
}
|
|
51
|
+
function resetTitle() {
|
|
52
|
+
if (!canWriteTitle())
|
|
53
|
+
return;
|
|
54
|
+
lastSetTitle = null;
|
|
55
|
+
writeOsc(RESET_TITLE);
|
|
56
|
+
}
|
|
57
|
+
exports.default = { setTitle, resetTitle };
|
package/dist/upload.d.ts
CHANGED
|
@@ -20,6 +20,10 @@ export default class Upload {
|
|
|
20
20
|
* Format file size in human-readable format (KB for small files, MB for larger)
|
|
21
21
|
*/
|
|
22
22
|
private formatFileSize;
|
|
23
|
+
/**
|
|
24
|
+
* Format seconds as compact duration: "12s" or "3m04s".
|
|
25
|
+
*/
|
|
26
|
+
private formatDuration;
|
|
23
27
|
private validateFile;
|
|
24
28
|
/**
|
|
25
29
|
* Validate that the file is a valid zip-based archive.
|
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;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;
|
|
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;IAkJlE,OAAO,CAAC,eAAe;IAuCvB;;OAEG;IACH,OAAO,CAAC,cAAc;IAUtB;;OAEG;IACH,OAAO,CAAC,cAAc;YAOR,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"}
|