@testingbot/cli 1.0.2 → 1.0.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/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.js +11 -13
- package/dist/providers/maestro.d.ts +20 -0
- package/dist/providers/maestro.d.ts.map +1 -1
- package/dist/providers/maestro.js +248 -33
- package/dist/providers/xcuitest.js +11 -13
- 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.js +7 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,9 +1,56 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://testingbot.com">
|
|
3
|
+
<img src="resources/logo.svg" alt="Maestro Cloud Testing" width="400">
|
|
4
|
+
</a>
|
|
5
|
+
<br>
|
|
6
|
+
<strong>Run mobile tests on real devices in the cloud</strong>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://github.com/testingbot/testingbotctl/actions/workflows/test.yml">
|
|
11
|
+
<img src="https://github.com/testingbot/testingbotctl/actions/workflows/test.yml/badge.svg" alt="Run Tests">
|
|
12
|
+
</a>
|
|
13
|
+
<a href="https://www.npmjs.com/package/@testingbot/cli">
|
|
14
|
+
<img src="https://img.shields.io/npm/v/@testingbot/cli.svg" alt="npm version">
|
|
15
|
+
</a>
|
|
16
|
+
<a href="https://www.npmjs.com/package/@testingbot/cli">
|
|
17
|
+
<img src="https://img.shields.io/npm/dm/@testingbot/cli.svg" alt="npm downloads">
|
|
18
|
+
</a>
|
|
19
|
+
<a href="https://github.com/testingbot/testingbotctl/blob/main/LICENSE">
|
|
20
|
+
<img src="https://img.shields.io/npm/l/@testingbot/cli.svg" alt="license">
|
|
21
|
+
</a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<p align="center">
|
|
25
|
+
<a href="https://testingbot.com">Website</a>
|
|
26
|
+
·
|
|
27
|
+
<a href="https://testingbot.com/support/app-automate/maestro">Documentation</a>
|
|
28
|
+
·
|
|
29
|
+
<a href="https://www.npmjs.com/package/@testingbot/cli">npm</a>
|
|
30
|
+
</p>
|
|
3
31
|
|
|
4
|
-
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
<p align="center">
|
|
35
|
+
<img src="demo/demo.gif" alt="TestingBot CLI Demo" width="700">
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
Run **Espresso**, **XCUITest** and **Maestro** tests on real devices in the cloud.
|
|
5
41
|
|
|
6
|
-
|
|
42
|
+
- **Real Devices** — Test on thousands of real iOS and Android devices
|
|
43
|
+
- **Emulators & Simulators** — Fast feedback with virtual devices
|
|
44
|
+
- **Parallel Execution** — Split tests across multiple devices with sharding
|
|
45
|
+
- **CI/CD Ready** — Integrates with GitHub Actions, Jenkins, and more
|
|
46
|
+
- **Live Results** — Watch tests run in real-time
|
|
47
|
+
- **Artifacts** — Download videos, screenshots, and logs
|
|
48
|
+
|
|
49
|
+
<p align="center">
|
|
50
|
+
<a href="#installation"><strong>Get Started →</strong></a>
|
|
51
|
+
</p>
|
|
52
|
+
|
|
53
|
+
---
|
|
7
54
|
|
|
8
55
|
## Installation
|
|
9
56
|
|
|
@@ -83,9 +130,25 @@ testingbot maestro <app> <flows...> [options]
|
|
|
83
130
|
| `-q, --quiet` | Suppress progress output |
|
|
84
131
|
| `--report <format>` | Download report after completion: html or junit |
|
|
85
132
|
| `--report-output-dir <path>` | Directory to save reports (required with --report) |
|
|
86
|
-
| `--download-artifacts` | Download test artifacts (logs, screenshots, video) |
|
|
133
|
+
| `--download-artifacts [mode]` | Download test artifacts (logs, screenshots, video). Mode: `all` (default) or `failed` |
|
|
87
134
|
| `--artifacts-output-dir <path>` | Directory to save artifacts zip (defaults to current directory) |
|
|
88
135
|
|
|
136
|
+
**Advanced Options:**
|
|
137
|
+
|
|
138
|
+
| Option | Description |
|
|
139
|
+
|--------|-------------|
|
|
140
|
+
| `--shard-split <number>` | Split flows into N parallel sessions for faster execution |
|
|
141
|
+
| `--ignore-checksum-check` | Skip checksum verification and always upload the app |
|
|
142
|
+
|
|
143
|
+
**CI/CD Integration:**
|
|
144
|
+
|
|
145
|
+
| Option | Description |
|
|
146
|
+
|--------|-------------|
|
|
147
|
+
| `--commit-sha <sha>` | Git commit SHA associated with this test run |
|
|
148
|
+
| `--pull-request-id <id>` | Pull request ID this test run originated from |
|
|
149
|
+
| `--repo-name <name>` | Repository name (e.g., GitHub repo slug) |
|
|
150
|
+
| `--repo-owner <owner>` | Repository owner (e.g., GitHub organization or username) |
|
|
151
|
+
|
|
89
152
|
**Examples:**
|
|
90
153
|
|
|
91
154
|
```sh
|
|
@@ -110,8 +173,21 @@ testingbot maestro app.apk ./flows --report junit --report-output-dir ./reports
|
|
|
110
173
|
# Download all artifacts (logs, screenshots, video)
|
|
111
174
|
testingbot maestro app.apk ./flows --download-artifacts --build "build-123"
|
|
112
175
|
|
|
176
|
+
# Download artifacts only for failed tests
|
|
177
|
+
testingbot maestro app.apk ./flows --download-artifacts failed --artifacts-output-dir ./artifacts
|
|
178
|
+
|
|
113
179
|
# Run in background (async)
|
|
114
180
|
testingbot maestro app.apk ./flows --async
|
|
181
|
+
|
|
182
|
+
# Split flows across 3 shards, grouping all flows over 3 parallel sessions
|
|
183
|
+
testingbot maestro app.apk ./flows --shard-split 3
|
|
184
|
+
|
|
185
|
+
# CI/CD integration with Git metadata
|
|
186
|
+
testingbot maestro app.apk ./flows \
|
|
187
|
+
--commit-sha "abc123def" \
|
|
188
|
+
--pull-request-id "42" \
|
|
189
|
+
--repo-owner "myorg" \
|
|
190
|
+
--repo-name "myapp"
|
|
115
191
|
```
|
|
116
192
|
|
|
117
193
|
---
|
|
@@ -325,7 +401,7 @@ testingbot xcuitest app.ipa app-test.zip \
|
|
|
325
401
|
### Real-time Progress
|
|
326
402
|
|
|
327
403
|
By default, the CLI shows real-time progress updates including:
|
|
328
|
-
- Test status updates
|
|
404
|
+
- Test status updates with actual device names (even when using wildcards)
|
|
329
405
|
- Device allocation status
|
|
330
406
|
- Live output from Maestro flows
|
|
331
407
|
|
package/dist/logger.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const
|
|
6
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
7
7
|
const tracer_1 = __importDefault(require("tracer"));
|
|
8
8
|
const logger = tracer_1.default.colorConsole({
|
|
9
9
|
level: 'info',
|
|
@@ -11,9 +11,9 @@ const logger = tracer_1.default.colorConsole({
|
|
|
11
11
|
dateformat: 'HH:MM:ss.L',
|
|
12
12
|
filters: [
|
|
13
13
|
{
|
|
14
|
-
warn:
|
|
15
|
-
debug:
|
|
16
|
-
error:
|
|
14
|
+
warn: picocolors_1.default.red,
|
|
15
|
+
debug: picocolors_1.default.blue,
|
|
16
|
+
error: (text) => picocolors_1.default.bold(picocolors_1.default.red(text)),
|
|
17
17
|
},
|
|
18
18
|
],
|
|
19
19
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"maestro_options.d.ts","sourceRoot":"","sources":["../../src/models/maestro_options.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"maestro_options.d.ts","sourceRoot":"","sources":["../../src/models/maestro_options.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
class MaestroOptions {
|
|
4
|
+
static isIpaFile(app) {
|
|
5
|
+
return app?.toLowerCase().endsWith('.ipa') ?? false;
|
|
6
|
+
}
|
|
4
7
|
_app;
|
|
5
8
|
_flows;
|
|
6
9
|
_device;
|
|
@@ -47,7 +50,8 @@ class MaestroOptions {
|
|
|
47
50
|
this._async = options?.async ?? false;
|
|
48
51
|
this._report = options?.report;
|
|
49
52
|
this._reportOutputDir = options?.reportOutputDir;
|
|
50
|
-
|
|
53
|
+
// IPA files can only be tested on real iOS devices, so automatically enable realDevice
|
|
54
|
+
this._realDevice = MaestroOptions.isIpaFile(app) || (options?.realDevice ?? false);
|
|
51
55
|
this._downloadArtifacts = options?.downloadArtifacts;
|
|
52
56
|
this._artifactsOutputDir = options?.artifactsOutputDir;
|
|
53
57
|
this._ignoreChecksumCheck = options?.ignoreChecksumCheck ?? false;
|
|
@@ -23,28 +23,26 @@ class Espresso extends base_provider_1.default {
|
|
|
23
23
|
if (this.options.app === undefined) {
|
|
24
24
|
throw new testingbot_error_1.default(`app option is required`);
|
|
25
25
|
}
|
|
26
|
-
try {
|
|
27
|
-
await node_fs_1.default.promises.access(this.options.app, node_fs_1.default.constants.R_OK);
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
throw new testingbot_error_1.default(`Provided app path does not exist ${this.options.app}`);
|
|
31
|
-
}
|
|
32
26
|
if (this.options.testApp === undefined) {
|
|
33
27
|
throw new testingbot_error_1.default(`testApp option is required`);
|
|
34
28
|
}
|
|
35
|
-
try {
|
|
36
|
-
await node_fs_1.default.promises.access(this.options.testApp, node_fs_1.default.constants.R_OK);
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
throw new testingbot_error_1.default(`testApp path does not exist ${this.options.testApp}`);
|
|
40
|
-
}
|
|
41
29
|
// Validate report options
|
|
42
30
|
if (this.options.report && !this.options.reportOutputDir) {
|
|
43
31
|
throw new testingbot_error_1.default(`--report-output-dir is required when --report is specified`);
|
|
44
32
|
}
|
|
33
|
+
// Validate file access in parallel for better performance
|
|
34
|
+
const fileChecks = [
|
|
35
|
+
node_fs_1.default.promises.access(this.options.app, node_fs_1.default.constants.R_OK).catch(() => {
|
|
36
|
+
throw new testingbot_error_1.default(`Provided app path does not exist ${this.options.app}`);
|
|
37
|
+
}),
|
|
38
|
+
node_fs_1.default.promises.access(this.options.testApp, node_fs_1.default.constants.R_OK).catch(() => {
|
|
39
|
+
throw new testingbot_error_1.default(`testApp path does not exist ${this.options.testApp}`);
|
|
40
|
+
}),
|
|
41
|
+
];
|
|
45
42
|
if (this.options.reportOutputDir) {
|
|
46
|
-
|
|
43
|
+
fileChecks.push(this.ensureOutputDirectory(this.options.reportOutputDir));
|
|
47
44
|
}
|
|
45
|
+
await Promise.all(fileChecks);
|
|
48
46
|
return true;
|
|
49
47
|
}
|
|
50
48
|
async run() {
|
|
@@ -56,6 +56,11 @@ export interface MaestroSocketMessage {
|
|
|
56
56
|
id: number;
|
|
57
57
|
payload: string;
|
|
58
58
|
}
|
|
59
|
+
export interface MissingFileReference {
|
|
60
|
+
flowFile: string;
|
|
61
|
+
referencedFile: string;
|
|
62
|
+
resolvedPath: string;
|
|
63
|
+
}
|
|
59
64
|
export default class Maestro extends BaseProvider<MaestroOptions> {
|
|
60
65
|
protected readonly URL = "https://api.testingbot.com/v1/app-automate/maestro";
|
|
61
66
|
private detectedPlatform;
|
|
@@ -63,6 +68,7 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
|
|
|
63
68
|
private updateServer;
|
|
64
69
|
private updateKey;
|
|
65
70
|
constructor(credentials: Credentials, options: MaestroOptions);
|
|
71
|
+
private static readonly SUPPORTED_APP_EXTENSIONS;
|
|
66
72
|
private validate;
|
|
67
73
|
/**
|
|
68
74
|
* Detect platform from app file content using magic bytes
|
|
@@ -86,6 +92,20 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
|
|
|
86
92
|
* Recursively extract file paths from any value in the YAML structure
|
|
87
93
|
*/
|
|
88
94
|
private extractPathsFromValue;
|
|
95
|
+
/**
|
|
96
|
+
* Find all file references in flow files that don't exist on disk.
|
|
97
|
+
* This validates that all referenced files (runScript, runFlow, addMedia, etc.)
|
|
98
|
+
* will be included in the zip.
|
|
99
|
+
*/
|
|
100
|
+
findMissingReferences(flowFiles: string[], allIncludedFiles: string[], baseDir?: string): Promise<MissingFileReference[]>;
|
|
101
|
+
/**
|
|
102
|
+
* Recursively find missing file references in a YAML value
|
|
103
|
+
*/
|
|
104
|
+
private findMissingInValue;
|
|
105
|
+
/**
|
|
106
|
+
* Log warnings for missing file references
|
|
107
|
+
*/
|
|
108
|
+
private logMissingReferences;
|
|
89
109
|
private logIncludedFiles;
|
|
90
110
|
private createFlowsZip;
|
|
91
111
|
private runTests;
|
|
@@ -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;AAahD,OAAO,YAAY,MAAM,iBAAiB,CAAC;AAE3C,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;AAExE,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,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,qBAAqB,CAAC;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,KAAK,CAAC,EAAE,eAAe,EAAE,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,iBAAkB,SAAQ,cAAc;IACvD,SAAS,EAAE,OAAO,CAAC;IACnB,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,cAAc,EAAE,CAAC;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,cAAc,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,CAAC,OAAO,OAAO,OAAQ,SAAQ,YAAY,CAAC,cAAc,CAAC;IAC/D,SAAS,CAAC,QAAQ,CAAC,GAAG,wDAAwD;IAE9E,OAAO,CAAC,gBAAgB,CAA4C;IACpE,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,SAAS,CAAuB;gBAErB,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc;
|
|
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;AAahD,OAAO,YAAY,MAAM,iBAAiB,CAAC;AAE3C,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;AAExE,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,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,qBAAqB,CAAC;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,KAAK,CAAC,EAAE,eAAe,EAAE,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,iBAAkB,SAAQ,cAAc;IACvD,SAAS,EAAE,OAAO,CAAC;IACnB,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,cAAc,EAAE,CAAC;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,cAAc,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,CAAC,OAAO,OAAO,OAAQ,SAAQ,YAAY,CAAC,cAAc,CAAC;IAC/D,SAAS,CAAC,QAAQ,CAAC,GAAG,wDAAwD;IAE9E,OAAO,CAAC,gBAAgB,CAA4C;IACpE,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,SAAS,CAAuB;gBAErB,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc;IAIpE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,wBAAwB,CAM9C;YAEY,QAAQ;IAgEtB;;OAEG;YACW,cAAc;IAOf,GAAG,IAAI,OAAO,CAAC,aAAa,CAAC;YAmE5B,SAAS;YAgDT,gBAAgB;YAkChB,WAAW;YAiHX,aAAa;YAqDb,oBAAoB;IA0ClC;;OAEG;IACH,OAAO,CAAC,aAAa;IAyBrB;;OAEG;YACW,gBAAgB;IAsC9B;;OAEG;YACW,qBAAqB;IAqLnC;;;;OAIG;IACU,qBAAqB,CAChC,SAAS,EAAE,MAAM,EAAE,EACnB,gBAAgB,EAAE,MAAM,EAAE,EAC1B,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAiClC;;OAEG;YACW,kBAAkB;IAgLhC;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAwB5B,OAAO,CAAC,gBAAgB;YAkDV,cAAc;YAiCd,QAAQ;YA6DR,SAAS;YA2BT,iBAAiB;IAiK/B,OAAO,CAAC,gBAAgB;IAsCxB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,aAAa;IAkBrB,OAAO,CAAC,oBAAoB;IAsB5B,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,qBAAqB;IAkB7B,OAAO,CAAC,iBAAiB;IAKzB,OAAO,CAAC,sBAAsB;IAO9B,OAAO,CAAC,mBAAmB;IA6C3B,OAAO,CAAC,qBAAqB;IAwB7B,OAAO,CAAC,uBAAuB;IAa/B,OAAO,CAAC,cAAc;IA6CtB,OAAO,CAAC,iBAAiB;IA0BzB,OAAO,CAAC,kBAAkB;YA2CZ,YAAY;YA6DZ,aAAa;YAiCb,oBAAoB;YAoBpB,YAAY;YA0DZ,uBAAuB;YAqBvB,iBAAiB;YAwLjB,sBAAsB;IAiBpC,OAAO,CAAC,qBAAqB;IAqC7B,OAAO,CAAC,0BAA0B;IAOlC,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,kBAAkB;CAa3B"}
|
|
@@ -48,7 +48,7 @@ const socket_io_client_1 = require("socket.io-client");
|
|
|
48
48
|
const testingbot_error_1 = __importDefault(require("../models/testingbot_error"));
|
|
49
49
|
const utils_1 = __importDefault(require("../utils"));
|
|
50
50
|
const file_type_detector_1 = require("../utils/file-type-detector");
|
|
51
|
-
const
|
|
51
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
52
52
|
const base_provider_1 = __importDefault(require("./base_provider"));
|
|
53
53
|
class Maestro extends base_provider_1.default {
|
|
54
54
|
URL = 'https://api.testingbot.com/v1/app-automate/maestro';
|
|
@@ -59,42 +59,53 @@ class Maestro extends base_provider_1.default {
|
|
|
59
59
|
constructor(credentials, options) {
|
|
60
60
|
super(credentials, options);
|
|
61
61
|
}
|
|
62
|
+
static SUPPORTED_APP_EXTENSIONS = [
|
|
63
|
+
'.apk',
|
|
64
|
+
'.apks',
|
|
65
|
+
'.ipa',
|
|
66
|
+
'.app',
|
|
67
|
+
'.zip',
|
|
68
|
+
];
|
|
62
69
|
async validate() {
|
|
63
70
|
if (this.options.app === undefined) {
|
|
64
71
|
throw new testingbot_error_1.default(`app option is required`);
|
|
65
72
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
// Validate app file extension
|
|
74
|
+
const appExt = node_path_1.default.extname(this.options.app).toLowerCase();
|
|
75
|
+
if (!Maestro.SUPPORTED_APP_EXTENSIONS.includes(appExt)) {
|
|
76
|
+
throw new testingbot_error_1.default(`Unsupported app file format: ${appExt || '(no extension)'}. ` +
|
|
77
|
+
`Supported formats: ${Maestro.SUPPORTED_APP_EXTENSIONS.join(', ')}`);
|
|
71
78
|
}
|
|
72
79
|
if (this.options.flows === undefined || this.options.flows.length === 0) {
|
|
73
80
|
throw new testingbot_error_1.default(`flows option is required`);
|
|
74
81
|
}
|
|
82
|
+
if (this.options.report && !this.options.reportOutputDir) {
|
|
83
|
+
throw new testingbot_error_1.default(`--report-output-dir is required when --report is specified`);
|
|
84
|
+
}
|
|
85
|
+
// Build list of all file checks to run in parallel
|
|
86
|
+
const fileChecks = [
|
|
87
|
+
node_fs_1.default.promises.access(this.options.app, node_fs_1.default.constants.R_OK).catch(() => {
|
|
88
|
+
throw new testingbot_error_1.default(`Provided app path does not exist ${this.options.app}`);
|
|
89
|
+
}),
|
|
90
|
+
];
|
|
75
91
|
// Check if all flows paths exist (can be files, directories or glob patterns)
|
|
76
92
|
for (const flowsPath of this.options.flows) {
|
|
77
93
|
const isGlobPattern = flowsPath.includes('*') ||
|
|
78
94
|
flowsPath.includes('?') ||
|
|
79
95
|
flowsPath.includes('{');
|
|
80
96
|
if (!isGlobPattern) {
|
|
81
|
-
|
|
82
|
-
await node_fs_1.default.promises.access(flowsPath, node_fs_1.default.constants.R_OK);
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
97
|
+
fileChecks.push(node_fs_1.default.promises.access(flowsPath, node_fs_1.default.constants.R_OK).catch(() => {
|
|
85
98
|
throw new testingbot_error_1.default(`flows path does not exist ${flowsPath}`);
|
|
86
|
-
}
|
|
99
|
+
}));
|
|
87
100
|
}
|
|
88
101
|
}
|
|
89
|
-
if (this.options.report && !this.options.reportOutputDir) {
|
|
90
|
-
throw new testingbot_error_1.default(`--report-output-dir is required when --report is specified`);
|
|
91
|
-
}
|
|
92
102
|
if (this.options.reportOutputDir) {
|
|
93
|
-
|
|
103
|
+
fileChecks.push(this.ensureOutputDirectory(this.options.reportOutputDir));
|
|
94
104
|
}
|
|
95
105
|
if (this.options.downloadArtifacts && this.options.artifactsOutputDir) {
|
|
96
|
-
|
|
106
|
+
fileChecks.push(this.ensureOutputDirectory(this.options.artifactsOutputDir));
|
|
97
107
|
}
|
|
108
|
+
await Promise.all(fileChecks);
|
|
98
109
|
return true;
|
|
99
110
|
}
|
|
100
111
|
/**
|
|
@@ -293,6 +304,11 @@ class Maestro extends base_provider_1.default {
|
|
|
293
304
|
if (!this.options.quiet) {
|
|
294
305
|
this.logIncludedFiles(allFlowFiles, baseDir);
|
|
295
306
|
}
|
|
307
|
+
// Check for missing file references and warn the user
|
|
308
|
+
const missingReferences = await this.findMissingReferences(allFlowFiles, allFlowFiles, baseDir);
|
|
309
|
+
if (!this.options.quiet && missingReferences.length > 0) {
|
|
310
|
+
this.logMissingReferences(missingReferences, baseDir);
|
|
311
|
+
}
|
|
296
312
|
zipPath = await this.createFlowsZip(allFlowFiles, baseDir);
|
|
297
313
|
shouldCleanup = true;
|
|
298
314
|
try {
|
|
@@ -539,6 +555,205 @@ class Maestro extends base_provider_1.default {
|
|
|
539
555
|
}
|
|
540
556
|
return dependencies;
|
|
541
557
|
}
|
|
558
|
+
/**
|
|
559
|
+
* Find all file references in flow files that don't exist on disk.
|
|
560
|
+
* This validates that all referenced files (runScript, runFlow, addMedia, etc.)
|
|
561
|
+
* will be included in the zip.
|
|
562
|
+
*/
|
|
563
|
+
async findMissingReferences(flowFiles, allIncludedFiles, baseDir) {
|
|
564
|
+
const missingReferences = [];
|
|
565
|
+
const includedFilesSet = new Set(allIncludedFiles.map((f) => node_path_1.default.resolve(f)));
|
|
566
|
+
for (const flowFile of flowFiles) {
|
|
567
|
+
const ext = node_path_1.default.extname(flowFile).toLowerCase();
|
|
568
|
+
if (ext !== '.yaml' && ext !== '.yml') {
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
try {
|
|
572
|
+
const content = await node_fs_1.default.promises.readFile(flowFile, 'utf-8');
|
|
573
|
+
const documents = [];
|
|
574
|
+
yaml.loadAll(content, (doc) => documents.push(doc));
|
|
575
|
+
for (const flowData of documents) {
|
|
576
|
+
if (flowData !== null && typeof flowData === 'object') {
|
|
577
|
+
const missing = await this.findMissingInValue(flowData, flowFile, includedFilesSet);
|
|
578
|
+
missingReferences.push(...missing);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
catch {
|
|
583
|
+
// Ignore parsing errors
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return missingReferences;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Recursively find missing file references in a YAML value
|
|
590
|
+
*/
|
|
591
|
+
async findMissingInValue(value, flowFile, includedFiles) {
|
|
592
|
+
const missingReferences = [];
|
|
593
|
+
if (typeof value === 'string') {
|
|
594
|
+
if (this.looksLikePath(value)) {
|
|
595
|
+
const resolvedPath = node_path_1.default.resolve(node_path_1.default.dirname(flowFile), value);
|
|
596
|
+
// Check if the file is in included files OR exists on disk
|
|
597
|
+
if (!includedFiles.has(resolvedPath)) {
|
|
598
|
+
try {
|
|
599
|
+
await node_fs_1.default.promises.access(resolvedPath);
|
|
600
|
+
// File exists on disk but won't be included - also warn
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
// File doesn't exist
|
|
604
|
+
missingReferences.push({
|
|
605
|
+
flowFile,
|
|
606
|
+
referencedFile: value,
|
|
607
|
+
resolvedPath,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
else if (Array.isArray(value)) {
|
|
614
|
+
for (const item of value) {
|
|
615
|
+
const missing = await this.findMissingInValue(item, flowFile, includedFiles);
|
|
616
|
+
missingReferences.push(...missing);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
else if (value !== null && typeof value === 'object') {
|
|
620
|
+
const obj = value;
|
|
621
|
+
const handledKeys = new Set();
|
|
622
|
+
// Handle runScript - extract file reference but don't recurse
|
|
623
|
+
// (runScript objects only contain file, env, when - no nested file refs)
|
|
624
|
+
if ('runScript' in obj) {
|
|
625
|
+
handledKeys.add('runScript');
|
|
626
|
+
const runScript = obj.runScript;
|
|
627
|
+
const scriptFile = typeof runScript === 'string'
|
|
628
|
+
? runScript
|
|
629
|
+
: runScript?.file;
|
|
630
|
+
if (typeof scriptFile === 'string') {
|
|
631
|
+
const resolved = node_path_1.default.resolve(node_path_1.default.dirname(flowFile), scriptFile);
|
|
632
|
+
if (!includedFiles.has(resolved)) {
|
|
633
|
+
try {
|
|
634
|
+
await node_fs_1.default.promises.access(resolved);
|
|
635
|
+
}
|
|
636
|
+
catch {
|
|
637
|
+
missingReferences.push({
|
|
638
|
+
flowFile,
|
|
639
|
+
referencedFile: scriptFile,
|
|
640
|
+
resolvedPath: resolved,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
// Don't recurse into runScript - it only has file, env, when (no nested file refs)
|
|
646
|
+
}
|
|
647
|
+
// Handle runFlow - extract file reference and recurse only into commands
|
|
648
|
+
if ('runFlow' in obj) {
|
|
649
|
+
handledKeys.add('runFlow');
|
|
650
|
+
const runFlow = obj.runFlow;
|
|
651
|
+
const flowRef = typeof runFlow === 'string'
|
|
652
|
+
? runFlow
|
|
653
|
+
: runFlow?.file;
|
|
654
|
+
if (typeof flowRef === 'string') {
|
|
655
|
+
const resolved = node_path_1.default.resolve(node_path_1.default.dirname(flowFile), flowRef);
|
|
656
|
+
if (!includedFiles.has(resolved)) {
|
|
657
|
+
try {
|
|
658
|
+
await node_fs_1.default.promises.access(resolved);
|
|
659
|
+
}
|
|
660
|
+
catch {
|
|
661
|
+
missingReferences.push({
|
|
662
|
+
flowFile,
|
|
663
|
+
referencedFile: flowRef,
|
|
664
|
+
resolvedPath: resolved,
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
// Only recurse into 'commands' if present (for inline commands)
|
|
670
|
+
if (typeof runFlow === 'object' &&
|
|
671
|
+
runFlow !== null &&
|
|
672
|
+
'commands' in runFlow) {
|
|
673
|
+
const commands = runFlow.commands;
|
|
674
|
+
if (Array.isArray(commands)) {
|
|
675
|
+
const nestedMissing = await this.findMissingInValue(commands, flowFile, includedFiles);
|
|
676
|
+
missingReferences.push(...nestedMissing);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
// Handle addMedia
|
|
681
|
+
if ('addMedia' in obj) {
|
|
682
|
+
handledKeys.add('addMedia');
|
|
683
|
+
const addMedia = obj.addMedia;
|
|
684
|
+
const mediaFiles = Array.isArray(addMedia) ? addMedia : [addMedia];
|
|
685
|
+
for (const mediaFile of mediaFiles) {
|
|
686
|
+
if (typeof mediaFile === 'string') {
|
|
687
|
+
const resolved = node_path_1.default.resolve(node_path_1.default.dirname(flowFile), mediaFile);
|
|
688
|
+
if (!includedFiles.has(resolved)) {
|
|
689
|
+
try {
|
|
690
|
+
await node_fs_1.default.promises.access(resolved);
|
|
691
|
+
}
|
|
692
|
+
catch {
|
|
693
|
+
missingReferences.push({
|
|
694
|
+
flowFile,
|
|
695
|
+
referencedFile: mediaFile,
|
|
696
|
+
resolvedPath: resolved,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
// Handle file property
|
|
704
|
+
if ('file' in obj && typeof obj.file === 'string') {
|
|
705
|
+
handledKeys.add('file');
|
|
706
|
+
const resolved = node_path_1.default.resolve(node_path_1.default.dirname(flowFile), obj.file);
|
|
707
|
+
if (!includedFiles.has(resolved)) {
|
|
708
|
+
try {
|
|
709
|
+
await node_fs_1.default.promises.access(resolved);
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
missingReferences.push({
|
|
713
|
+
flowFile,
|
|
714
|
+
referencedFile: obj.file,
|
|
715
|
+
resolvedPath: resolved,
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
// Handle onFlowStart, onFlowComplete, commands
|
|
721
|
+
for (const key of ['onFlowStart', 'onFlowComplete', 'commands']) {
|
|
722
|
+
if (key in obj) {
|
|
723
|
+
handledKeys.add(key);
|
|
724
|
+
const nested = obj[key];
|
|
725
|
+
if (Array.isArray(nested)) {
|
|
726
|
+
const nestedMissing = await this.findMissingInValue(nested, flowFile, includedFiles);
|
|
727
|
+
missingReferences.push(...nestedMissing);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
// Recursively check remaining properties
|
|
732
|
+
for (const [key, propValue] of Object.entries(obj)) {
|
|
733
|
+
if (!handledKeys.has(key)) {
|
|
734
|
+
const nestedMissing = await this.findMissingInValue(propValue, flowFile, includedFiles);
|
|
735
|
+
missingReferences.push(...nestedMissing);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return missingReferences;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Log warnings for missing file references
|
|
743
|
+
*/
|
|
744
|
+
logMissingReferences(missingReferences, baseDir) {
|
|
745
|
+
if (missingReferences.length === 0) {
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
logger_1.default.warn(`Warning: ${missingReferences.length} referenced file(s) not found:`);
|
|
749
|
+
for (const ref of missingReferences) {
|
|
750
|
+
const flowRelative = baseDir
|
|
751
|
+
? node_path_1.default.relative(baseDir, ref.flowFile)
|
|
752
|
+
: node_path_1.default.basename(ref.flowFile);
|
|
753
|
+
logger_1.default.warn(` In ${flowRelative}: ${ref.referencedFile}`);
|
|
754
|
+
}
|
|
755
|
+
logger_1.default.warn('These files will not be included in the upload and may cause test failures.');
|
|
756
|
+
}
|
|
542
757
|
logIncludedFiles(files, baseDir) {
|
|
543
758
|
// Get relative paths for display
|
|
544
759
|
const relativePaths = files
|
|
@@ -753,9 +968,9 @@ class Maestro extends base_provider_1.default {
|
|
|
753
968
|
process.stdout.write(`\x1b[${linesToMove}A`);
|
|
754
969
|
// Clear header line, write new header, then clear separator line
|
|
755
970
|
process.stdout.write('\x1b[2K');
|
|
756
|
-
console.log(
|
|
971
|
+
console.log(picocolors_1.default.dim(` ${'Duration'.padEnd(10)} ${'Status'.padEnd(8)} Flow Fail reason`));
|
|
757
972
|
process.stdout.write('\x1b[2K');
|
|
758
|
-
console.log(
|
|
973
|
+
console.log(picocolors_1.default.dim(` ${'─'.repeat(10)} ${'─'.repeat(8)} ${'─'.repeat(30)} ${'─'.repeat(80)}`));
|
|
759
974
|
// Redraw all flows with error messages
|
|
760
975
|
for (const flow of allFlows) {
|
|
761
976
|
// Clear the line before writing
|
|
@@ -848,18 +1063,18 @@ class Maestro extends base_provider_1.default {
|
|
|
848
1063
|
getFlowStatusDisplay(flow) {
|
|
849
1064
|
switch (flow.status) {
|
|
850
1065
|
case 'WAITING':
|
|
851
|
-
return { text: 'WAITING', colored:
|
|
1066
|
+
return { text: 'WAITING', colored: picocolors_1.default.white('WAITING') };
|
|
852
1067
|
case 'READY':
|
|
853
|
-
return { text: 'RUNNING', colored:
|
|
1068
|
+
return { text: 'RUNNING', colored: picocolors_1.default.blue('RUNNING') };
|
|
854
1069
|
case 'DONE':
|
|
855
1070
|
if (flow.success === 1) {
|
|
856
|
-
return { text: 'PASSED', colored:
|
|
1071
|
+
return { text: 'PASSED', colored: picocolors_1.default.green('PASSED') };
|
|
857
1072
|
}
|
|
858
1073
|
else {
|
|
859
|
-
return { text: 'FAILED', colored:
|
|
1074
|
+
return { text: 'FAILED', colored: picocolors_1.default.red('FAILED') };
|
|
860
1075
|
}
|
|
861
1076
|
case 'FAILED':
|
|
862
|
-
return { text: 'FAILED', colored:
|
|
1077
|
+
return { text: 'FAILED', colored: picocolors_1.default.red('FAILED') };
|
|
863
1078
|
default:
|
|
864
1079
|
return { text: flow.status, colored: flow.status };
|
|
865
1080
|
}
|
|
@@ -927,13 +1142,13 @@ class Maestro extends base_provider_1.default {
|
|
|
927
1142
|
}
|
|
928
1143
|
const parts = [];
|
|
929
1144
|
if (waiting > 0)
|
|
930
|
-
parts.push(
|
|
1145
|
+
parts.push(picocolors_1.default.white(`${waiting} waiting`));
|
|
931
1146
|
if (running > 0)
|
|
932
|
-
parts.push(
|
|
1147
|
+
parts.push(picocolors_1.default.blue(`${running} running`));
|
|
933
1148
|
if (passed > 0)
|
|
934
|
-
parts.push(
|
|
1149
|
+
parts.push(picocolors_1.default.green(`${passed} passed`));
|
|
935
1150
|
if (failed > 0)
|
|
936
|
-
parts.push(
|
|
1151
|
+
parts.push(picocolors_1.default.red(`${failed} failed`));
|
|
937
1152
|
return ` ... and ${remaining.length} more: ${parts.join(', ')}`;
|
|
938
1153
|
}
|
|
939
1154
|
displayFlowsWithLimit(flows, previousFlowStatus, hasFailures = false) {
|
|
@@ -947,7 +1162,7 @@ class Maestro extends base_provider_1.default {
|
|
|
947
1162
|
// Show summary for remaining flows
|
|
948
1163
|
if (flows.length > maxFlows) {
|
|
949
1164
|
const summary = this.getRemainingSummary(flows, maxFlows);
|
|
950
|
-
console.log(
|
|
1165
|
+
console.log(picocolors_1.default.dim(summary));
|
|
951
1166
|
linesWritten++;
|
|
952
1167
|
}
|
|
953
1168
|
return linesWritten;
|
|
@@ -959,8 +1174,8 @@ class Maestro extends base_provider_1.default {
|
|
|
959
1174
|
header += ' Fail reason';
|
|
960
1175
|
separator += ` ${'─'.repeat(80)}`;
|
|
961
1176
|
}
|
|
962
|
-
console.log(
|
|
963
|
-
console.log(
|
|
1177
|
+
console.log(picocolors_1.default.dim(header));
|
|
1178
|
+
console.log(picocolors_1.default.dim(separator));
|
|
964
1179
|
}
|
|
965
1180
|
displayFlowRow(flow, isUpdate = false, hasFailures = false) {
|
|
966
1181
|
const duration = this.calculateFlowDuration(flow).padEnd(10);
|
|
@@ -976,7 +1191,7 @@ class Maestro extends base_provider_1.default {
|
|
|
976
1191
|
let row = ` ${duration} ${statusPadded} ${name}`;
|
|
977
1192
|
// Add first error message on the same line if failed and has errors
|
|
978
1193
|
if (hasFailures && isFailed && errorMessages.length > 0) {
|
|
979
|
-
row += ` ${
|
|
1194
|
+
row += ` ${picocolors_1.default.red(errorMessages[0])}`;
|
|
980
1195
|
}
|
|
981
1196
|
if (isUpdate) {
|
|
982
1197
|
process.stdout.write(`\r${row}`);
|
|
@@ -990,7 +1205,7 @@ class Maestro extends base_provider_1.default {
|
|
|
990
1205
|
// Indent to align with the Fail reason column: Duration(11) + Status(9) + Test(31) = 51 chars
|
|
991
1206
|
const indent = ' '.repeat(51);
|
|
992
1207
|
for (let i = 1; i < errorMessages.length; i++) {
|
|
993
|
-
console.log(`${indent} ${
|
|
1208
|
+
console.log(`${indent} ${picocolors_1.default.red(errorMessages[i])}`);
|
|
994
1209
|
linesWritten++;
|
|
995
1210
|
}
|
|
996
1211
|
}
|
|
@@ -1035,7 +1250,7 @@ class Maestro extends base_provider_1.default {
|
|
|
1035
1250
|
// Update or add summary line for remaining flows
|
|
1036
1251
|
if (hasRemaining) {
|
|
1037
1252
|
const summary = this.getRemainingSummary(flows, maxFlows);
|
|
1038
|
-
process.stdout.write(`\r\x1b[K${
|
|
1253
|
+
process.stdout.write(`\r\x1b[K${picocolors_1.default.dim(summary)}\n`);
|
|
1039
1254
|
linesWritten++;
|
|
1040
1255
|
}
|
|
1041
1256
|
// Return the number of lines we wrote
|
|
@@ -23,28 +23,26 @@ class XCUITest extends base_provider_1.default {
|
|
|
23
23
|
if (this.options.app === undefined) {
|
|
24
24
|
throw new testingbot_error_1.default(`app option is required`);
|
|
25
25
|
}
|
|
26
|
-
try {
|
|
27
|
-
await node_fs_1.default.promises.access(this.options.app, node_fs_1.default.constants.R_OK);
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
throw new testingbot_error_1.default(`Provided app path does not exist ${this.options.app}`);
|
|
31
|
-
}
|
|
32
26
|
if (this.options.testApp === undefined) {
|
|
33
27
|
throw new testingbot_error_1.default(`testApp option is required`);
|
|
34
28
|
}
|
|
35
|
-
try {
|
|
36
|
-
await node_fs_1.default.promises.access(this.options.testApp, node_fs_1.default.constants.R_OK);
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
throw new testingbot_error_1.default(`testApp path does not exist ${this.options.testApp}`);
|
|
40
|
-
}
|
|
41
29
|
// Validate report options
|
|
42
30
|
if (this.options.report && !this.options.reportOutputDir) {
|
|
43
31
|
throw new testingbot_error_1.default(`--report-output-dir is required when --report is specified`);
|
|
44
32
|
}
|
|
33
|
+
// Validate file access in parallel for better performance
|
|
34
|
+
const fileChecks = [
|
|
35
|
+
node_fs_1.default.promises.access(this.options.app, node_fs_1.default.constants.R_OK).catch(() => {
|
|
36
|
+
throw new testingbot_error_1.default(`Provided app path does not exist ${this.options.app}`);
|
|
37
|
+
}),
|
|
38
|
+
node_fs_1.default.promises.access(this.options.testApp, node_fs_1.default.constants.R_OK).catch(() => {
|
|
39
|
+
throw new testingbot_error_1.default(`testApp path does not exist ${this.options.testApp}`);
|
|
40
|
+
}),
|
|
41
|
+
];
|
|
45
42
|
if (this.options.reportOutputDir) {
|
|
46
|
-
|
|
43
|
+
fileChecks.push(this.ensureOutputDirectory(this.options.reportOutputDir));
|
|
47
44
|
}
|
|
45
|
+
await Promise.all(fileChecks);
|
|
48
46
|
return true;
|
|
49
47
|
}
|
|
50
48
|
async run() {
|
|
@@ -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.js
CHANGED
|
@@ -5,7 +5,7 @@ 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
10
|
exports.default = {
|
|
11
11
|
getUserAgent() {
|
|
@@ -42,12 +42,12 @@ exports.default = {
|
|
|
42
42
|
if (this.compareVersions(currentVersion, latestVersion) < 0) {
|
|
43
43
|
versionCheckDisplayed = true;
|
|
44
44
|
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(
|
|
45
|
+
logger_1.default.info(`\nCLI Version: ${picocolors_1.default.cyan(currentVersion)}\n`);
|
|
46
|
+
logger_1.default.warn(picocolors_1.default.yellow(border));
|
|
47
|
+
logger_1.default.warn(picocolors_1.default.yellow('⚠ Update Available'));
|
|
48
|
+
logger_1.default.warn(picocolors_1.default.yellow(` A new version of the TestingBot CLI is available: ${picocolors_1.default.green(latestVersion)}`));
|
|
49
|
+
logger_1.default.warn(picocolors_1.default.yellow(` Run: ${picocolors_1.default.cyan('npm install -g @testingbot/cli@latest')}`));
|
|
50
|
+
logger_1.default.warn(picocolors_1.default.yellow(border) + '\n');
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
53
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testingbot/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
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": {
|
|
@@ -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"
|