@testrigor/testrigor-cli 0.0.42-beta → 0.0.44-beta
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 +40 -0
- package/dist/commands/test-suite/run.d.ts +4 -0
- package/dist/commands/test-suite/run.js +142 -36
- package/dist/util/appium.d.ts +25 -0
- package/dist/util/appium.js +189 -0
- package/dist/util/trtc.d.ts +1 -0
- package/dist/util/trtc.js +5 -1
- package/dist/util/tunnel.d.ts +1 -0
- package/dist/util/tunnel.js +9 -9
- package/oclif.manifest.json +15 -9
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -31,3 +31,43 @@ This is a command line tool for TestRigor. It allows you to run TestRigor tests
|
|
|
31
31
|
20. Show current configuration: `testrigor test-suite config --show`
|
|
32
32
|
21. Authenticate with TestRigor and store credentials (interactive prompt): `testrigor authenticate`
|
|
33
33
|
22. Run with override chrome extensions at runtime (one or more chrome extensions): `test-suite run <test-suite-id> --token <token> --chrome-extension "name=<extension-name> file=<extension-file>" --chrome-extension "name=<extension-name> file=<extension-file>"`
|
|
34
|
+
|
|
35
|
+
### Mobile Testing
|
|
36
|
+
|
|
37
|
+
Run mobile tests on your own local devices (emulators/simulators/real devices) using the same `--localhost` flag. The CLI auto-detects mobile suites and handles everything behind the scenes (mobile server, tunnel, configuration).
|
|
38
|
+
|
|
39
|
+
#### Setup
|
|
40
|
+
|
|
41
|
+
Run your test suite with `--localhost` and `--local-test-nodes-path`:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
testrigor test-suite run <test-suite-id> --token <token> --localhost --local-test-nodes-path ./test-nodes.yaml
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The CLI will:
|
|
48
|
+
- Detect it's a mobile suite
|
|
49
|
+
- Start the mobile server (Appium) automatically
|
|
50
|
+
- Open a tunnel to TestRigor cloud
|
|
51
|
+
- Run the test on your local device
|
|
52
|
+
- Stop everything when done
|
|
53
|
+
|
|
54
|
+
#### Override device config with a YAML file
|
|
55
|
+
|
|
56
|
+
Instead of configuring devices in the UI, you can provide a local YAML file with `--local-test-nodes-path`:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
testrigor test-suite run <test-suite-id> --token <token> --local-test-nodes-path ./test-nodes.yaml
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Example `test-nodes.yaml`:
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
testNodes:
|
|
66
|
+
- os: iOS
|
|
67
|
+
osVersion: "18.4"
|
|
68
|
+
abi: x86_64
|
|
69
|
+
x86_64: "iPhone 16 Pro"
|
|
70
|
+
overrideTestNodes: true
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
This overrides the device config stored in the test suite and forces mobile mode (no need for `--localhost`).
|
|
@@ -17,6 +17,7 @@ export default class Run extends CommandBase<typeof Run> {
|
|
|
17
17
|
"test-cases-path": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
|
|
18
18
|
"rules-path": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
|
|
19
19
|
"settings-path": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
|
|
20
|
+
"local-test-nodes-path": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
|
|
20
21
|
"test-case-uuid": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
|
|
21
22
|
"auto-create-ai-rules": import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
22
23
|
"explicit-mutations": import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
@@ -28,6 +29,7 @@ export default class Run extends CommandBase<typeof Run> {
|
|
|
28
29
|
name: string;
|
|
29
30
|
file: string;
|
|
30
31
|
} | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
|
|
32
|
+
"xray-execution-key": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
|
|
31
33
|
};
|
|
32
34
|
static args: {
|
|
33
35
|
testSuiteId: import("@oclif/core/lib/interfaces/parser").Arg<string | undefined, Record<string, unknown>>;
|
|
@@ -37,6 +39,8 @@ export default class Run extends CommandBase<typeof Run> {
|
|
|
37
39
|
private fetchStatus;
|
|
38
40
|
private getStatusApiEndpoint;
|
|
39
41
|
private callRetest;
|
|
42
|
+
private fetchSuiteType;
|
|
43
|
+
private isMobileSuiteType;
|
|
40
44
|
private uploadFile;
|
|
41
45
|
private waitForTestSuiteToBeCreated;
|
|
42
46
|
private waitForTestSuiteToFinish;
|
|
@@ -11,6 +11,7 @@ const chrome_extension_1 = require("../../flags/chrome-extension");
|
|
|
11
11
|
const parser_1 = require("../../util/parser");
|
|
12
12
|
const request_builder_1 = require("../../util/request-builder");
|
|
13
13
|
const tunnel_1 = require("../../util/tunnel");
|
|
14
|
+
const appium_1 = require("../../util/appium");
|
|
14
15
|
const utils_1 = require("../../util/utils");
|
|
15
16
|
const sleep = async (milliseconds) => {
|
|
16
17
|
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
@@ -70,6 +71,7 @@ class Run extends command_base_1.CommandBase {
|
|
|
70
71
|
const maxPort = flags["max-port"];
|
|
71
72
|
const url = flags["url"];
|
|
72
73
|
const filePath = flags["file-path"];
|
|
74
|
+
const localTestNodesPath = flags["local-test-nodes-path"];
|
|
73
75
|
const variablesPath = flags["variables-path"];
|
|
74
76
|
const testCasesPath = flags["test-cases-path"];
|
|
75
77
|
const rulesPath = flags["rules-path"];
|
|
@@ -79,6 +81,7 @@ class Run extends command_base_1.CommandBase {
|
|
|
79
81
|
const explicitMutations = flags["explicit-mutations"];
|
|
80
82
|
const labels = flags["labels"];
|
|
81
83
|
const excludedLabels = flags["excluded-labels"];
|
|
84
|
+
const xrayExecutionKey = flags["xray-execution-key"];
|
|
82
85
|
const chromeExtensions = flags["chrome-extension"];
|
|
83
86
|
const context = {
|
|
84
87
|
configuration: {
|
|
@@ -87,6 +90,8 @@ class Run extends command_base_1.CommandBase {
|
|
|
87
90
|
settings: {},
|
|
88
91
|
},
|
|
89
92
|
tunnel: new tunnel_1.default(),
|
|
93
|
+
isMobile: false,
|
|
94
|
+
appiumStartedByUs: false,
|
|
90
95
|
};
|
|
91
96
|
if (labels) {
|
|
92
97
|
context.configuration.labels = labels;
|
|
@@ -94,6 +99,9 @@ class Run extends command_base_1.CommandBase {
|
|
|
94
99
|
if (excludedLabels) {
|
|
95
100
|
context.configuration.excludedLabels = excludedLabels;
|
|
96
101
|
}
|
|
102
|
+
if (xrayExecutionKey) {
|
|
103
|
+
context.configuration.xrayExecutionKey = xrayExecutionKey;
|
|
104
|
+
}
|
|
97
105
|
if (minPort > maxPort) {
|
|
98
106
|
core_1.ux.error("--minPort cannot be greater than --maxPort");
|
|
99
107
|
}
|
|
@@ -149,6 +157,24 @@ class Run extends command_base_1.CommandBase {
|
|
|
149
157
|
core_1.ux.error("--settings-path must be a valid JSON/YAML path");
|
|
150
158
|
}
|
|
151
159
|
}
|
|
160
|
+
if (localTestNodesPath) {
|
|
161
|
+
try {
|
|
162
|
+
const localTestNodes = await (0, parser_1.parseSettings)(localTestNodesPath);
|
|
163
|
+
if (localTestNodes) {
|
|
164
|
+
if (Array.isArray(localTestNodes.testNodes)) {
|
|
165
|
+
for (const node of localTestNodes.testNodes) {
|
|
166
|
+
if (!node.provider) {
|
|
167
|
+
node.provider = "LOCALHOST";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
Object.assign(context.configuration, localTestNodes);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
core_1.ux.error("--local-test-nodes-path must be a valid JSON/YAML path");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
152
178
|
if (filePath) {
|
|
153
179
|
if (url) {
|
|
154
180
|
core_1.ux.info("WARNING: File path will be ignored because --url is used");
|
|
@@ -168,50 +194,85 @@ class Run extends command_base_1.CommandBase {
|
|
|
168
194
|
context.chromeExtensions = chromeExtensions;
|
|
169
195
|
}
|
|
170
196
|
super.setTunnel(context.tunnel);
|
|
197
|
+
if (localTestNodesPath) {
|
|
198
|
+
context.isMobile = true;
|
|
199
|
+
}
|
|
200
|
+
else if (flags.localhost) {
|
|
201
|
+
const suiteType = await this.fetchSuiteType(testSuiteId, token);
|
|
202
|
+
context.isMobile = this.isMobileSuiteType(suiteType);
|
|
203
|
+
}
|
|
171
204
|
const tasks = [
|
|
172
205
|
{
|
|
173
|
-
enabled: () => flags.localhost,
|
|
206
|
+
enabled: () => (flags.localhost || !!localTestNodesPath) && context.isMobile,
|
|
174
207
|
task: (ctx, task) => {
|
|
175
|
-
|
|
176
|
-
task.title = "Initializing tunnel";
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
task.title = "Initializing";
|
|
180
|
-
}
|
|
208
|
+
task.title = "Starting mobile server";
|
|
181
209
|
return task.newListr([
|
|
182
210
|
{
|
|
183
|
-
task: async (
|
|
184
|
-
|
|
185
|
-
|
|
211
|
+
task: async (_ctx, subtask) => {
|
|
212
|
+
await (0, appium_1.checkAppiumInstalled)();
|
|
213
|
+
subtask.title = "Mobile server binary found";
|
|
214
|
+
},
|
|
215
|
+
rendererOptions: { timer: listr2_1.PRESET_TIMER },
|
|
216
|
+
},
|
|
217
|
+
...appium_1.REQUIRED_DRIVERS.map((driver) => ({
|
|
218
|
+
task: async (_ctx, subtask) => {
|
|
219
|
+
const status = await (0, appium_1.getDriverStatus)(driver.name, driver.version);
|
|
220
|
+
if (status.installed && status.upToDate) {
|
|
221
|
+
subtask.title = `Driver ${driver.name}@${driver.version} ready`;
|
|
222
|
+
return;
|
|
186
223
|
}
|
|
187
|
-
|
|
224
|
+
subtask.title = status.installed
|
|
225
|
+
? `Updating ${driver.name} (${status.version} → ${driver.version})`
|
|
226
|
+
: `Installing ${driver.name}@${driver.version}`;
|
|
227
|
+
const err = await (0, appium_1.ensureDriver)(driver.name, driver.version);
|
|
228
|
+
subtask.title = err
|
|
229
|
+
? `Driver ${driver.name} skipped — ${err}`
|
|
230
|
+
: `Driver ${driver.name}@${driver.version} ready`;
|
|
231
|
+
},
|
|
232
|
+
rendererOptions: { timer: listr2_1.PRESET_TIMER },
|
|
233
|
+
})),
|
|
234
|
+
{
|
|
235
|
+
task: async (ctx, subtask) => {
|
|
236
|
+
const result = await (0, appium_1.startAppium)();
|
|
237
|
+
ctx.appiumStartedByUs = !result.alreadyRunning;
|
|
238
|
+
subtask.title = result.alreadyRunning
|
|
239
|
+
? `Mobile server already running (PID: ${result.pid}), reusing`
|
|
240
|
+
: `Mobile server started (PID: ${result.pid})`;
|
|
188
241
|
},
|
|
189
|
-
rendererOptions: {
|
|
190
|
-
|
|
242
|
+
rendererOptions: { timer: listr2_1.PRESET_TIMER },
|
|
243
|
+
},
|
|
244
|
+
], { concurrent: false, rendererOptions: { collapseSubtasks: false } });
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
enabled: () => flags.localhost || !!localTestNodesPath,
|
|
249
|
+
task: (ctx, task) => {
|
|
250
|
+
task.title = flags.verbose ? "Initializing tunnel" : "Initializing";
|
|
251
|
+
return task.newListr([
|
|
252
|
+
{
|
|
253
|
+
task: async (ctx, subtask) => {
|
|
254
|
+
await ctx.tunnel.initialize();
|
|
255
|
+
subtask.title = "Binary ready";
|
|
191
256
|
},
|
|
257
|
+
rendererOptions: { timer: listr2_1.PRESET_TIMER },
|
|
192
258
|
},
|
|
193
259
|
{
|
|
194
|
-
task: async (ctx,
|
|
195
|
-
if (flags.verbose) {
|
|
196
|
-
task.title = "Starting tunnel";
|
|
197
|
-
}
|
|
260
|
+
task: async (ctx, subtask) => {
|
|
198
261
|
await ctx.tunnel.start(minPort, maxPort);
|
|
262
|
+
subtask.title = `Tunnel started (port ${ctx.tunnel.getPort()})`;
|
|
199
263
|
},
|
|
200
264
|
exitOnError: true,
|
|
201
|
-
rendererOptions: {
|
|
202
|
-
timer: listr2_1.PRESET_TIMER,
|
|
203
|
-
},
|
|
265
|
+
rendererOptions: { timer: listr2_1.PRESET_TIMER },
|
|
204
266
|
},
|
|
205
267
|
{
|
|
206
268
|
enabled: (ctx) => ctx.tunnel.isRunning(),
|
|
207
|
-
task: (ctx,
|
|
208
|
-
|
|
209
|
-
task.title = `Tunnel started at port ${ctx.tunnel.getPort()}`;
|
|
210
|
-
}
|
|
269
|
+
task: (ctx, subtask) => {
|
|
270
|
+
const tunnelAddress = `tunnel.testrigor.com:${ctx.tunnel.getPort()}`;
|
|
211
271
|
ctx.configuration.settings = {
|
|
212
272
|
...ctx.configuration.settings,
|
|
213
|
-
httpProxyString:
|
|
273
|
+
httpProxyString: tunnelAddress,
|
|
214
274
|
};
|
|
275
|
+
subtask.title = `Tunnel connected (${tunnelAddress})`;
|
|
215
276
|
},
|
|
216
277
|
},
|
|
217
278
|
], { concurrent: false, rendererOptions: { collapseSubtasks: false } });
|
|
@@ -223,7 +284,11 @@ class Run extends command_base_1.CommandBase {
|
|
|
223
284
|
{
|
|
224
285
|
// Checking last run id
|
|
225
286
|
task: async (ctx) => {
|
|
226
|
-
|
|
287
|
+
let pollBranch = flags.branch;
|
|
288
|
+
if (testCaseUuid && !flags.branch) {
|
|
289
|
+
pollBranch = `single-test-execution-${testCaseUuid}`;
|
|
290
|
+
}
|
|
291
|
+
const statusResponse = await this.fetchStatus(testSuiteId, token, pollBranch, ctx.configuration.labels, ctx.configuration.excludedLabels);
|
|
227
292
|
context.lastRunId = (0, utils_1.extractRunIdFromDetailsUrl)(statusResponse.detailsUrl);
|
|
228
293
|
},
|
|
229
294
|
exitOnError: false,
|
|
@@ -385,7 +450,11 @@ class Run extends command_base_1.CommandBase {
|
|
|
385
450
|
title: "Waiting for test suite run creation",
|
|
386
451
|
enabled: (ctx) => ctx.response?.queueId,
|
|
387
452
|
task: async (ctx, currentTask) => {
|
|
388
|
-
|
|
453
|
+
let pollBranch = flags.branch;
|
|
454
|
+
if (testCaseUuid && !flags.branch) {
|
|
455
|
+
pollBranch = `single-test-execution-${testCaseUuid}`;
|
|
456
|
+
}
|
|
457
|
+
const detailsUrl = await this.waitForTestSuiteToBeCreated(testSuiteId, token, ctx.lastRunId, pollBranch, ctx.configuration.labels, ctx.configuration.excludedLabels);
|
|
389
458
|
task.title = `Running test suite [ ${detailsUrl} ]`;
|
|
390
459
|
currentTask.title = "Test suite run created successfully";
|
|
391
460
|
},
|
|
@@ -394,13 +463,19 @@ class Run extends command_base_1.CommandBase {
|
|
|
394
463
|
{
|
|
395
464
|
title: "Checking status",
|
|
396
465
|
enabled: (ctx) => ctx.response?.result === "ok" && !flags.async,
|
|
397
|
-
task: async (ctx, task) =>
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
466
|
+
task: async (ctx, task) => {
|
|
467
|
+
let pollBranch = flags.branch;
|
|
468
|
+
if (testCaseUuid && !flags.branch) {
|
|
469
|
+
pollBranch = `single-test-execution-${testCaseUuid}`;
|
|
470
|
+
}
|
|
471
|
+
return task.newListr([
|
|
472
|
+
{
|
|
473
|
+
task: async (ctx, statusTask) => {
|
|
474
|
+
await this.waitForTestSuiteToFinish(testSuiteId, token, statusTask, pollBranch, ctx.configuration.labels, ctx.configuration.excludedLabels);
|
|
475
|
+
},
|
|
401
476
|
},
|
|
402
|
-
|
|
403
|
-
|
|
477
|
+
]);
|
|
478
|
+
},
|
|
404
479
|
exitOnError: false,
|
|
405
480
|
},
|
|
406
481
|
{
|
|
@@ -438,6 +513,15 @@ class Run extends command_base_1.CommandBase {
|
|
|
438
513
|
await ctx.tunnel.stop();
|
|
439
514
|
},
|
|
440
515
|
},
|
|
516
|
+
{
|
|
517
|
+
enabled: (ctx) => ctx.appiumStartedByUs,
|
|
518
|
+
task: async (_ctx, task) => {
|
|
519
|
+
if (flags.verbose) {
|
|
520
|
+
task.title = "Stopping mobile server";
|
|
521
|
+
}
|
|
522
|
+
await (0, appium_1.stopAppium)();
|
|
523
|
+
},
|
|
524
|
+
},
|
|
441
525
|
];
|
|
442
526
|
const runner = new listr2_1.Listr(tasks, {
|
|
443
527
|
ctx: context,
|
|
@@ -491,6 +575,24 @@ class Run extends command_base_1.CommandBase {
|
|
|
491
575
|
},
|
|
492
576
|
});
|
|
493
577
|
}
|
|
578
|
+
async fetchSuiteType(testSuiteId, token) {
|
|
579
|
+
const response = await (0, node_fetch_commonjs_1.default)(`${this.TESTRIGOR_API_URL}/apps/${testSuiteId}/status`, {
|
|
580
|
+
method: "GET",
|
|
581
|
+
headers: {
|
|
582
|
+
Accept: "application/json",
|
|
583
|
+
"auth-token": token,
|
|
584
|
+
},
|
|
585
|
+
});
|
|
586
|
+
if (!response.ok) {
|
|
587
|
+
throw new Error(`Failed to fetch test suite info: ${response.statusText}`);
|
|
588
|
+
}
|
|
589
|
+
const data = (await response.json());
|
|
590
|
+
return data.type || "";
|
|
591
|
+
}
|
|
592
|
+
isMobileSuiteType(type) {
|
|
593
|
+
const mobilePrefixes = ["IOS_", "ANDROID_"];
|
|
594
|
+
return mobilePrefixes.some((prefix) => type.toUpperCase().startsWith(prefix));
|
|
595
|
+
}
|
|
494
596
|
async uploadFile(task, testSuiteId, configuration, filePath, token, fileProperties) {
|
|
495
597
|
const body = {
|
|
496
598
|
fileName: fileProperties.fileName,
|
|
@@ -664,9 +766,8 @@ Run.flags = {
|
|
|
664
766
|
dependsOn: ["branch"],
|
|
665
767
|
}),
|
|
666
768
|
localhost: core_1.Flags.boolean({
|
|
667
|
-
description: "Localhost run",
|
|
769
|
+
description: "Localhost run (auto-detects web vs mobile)",
|
|
668
770
|
aliases: ["tunnel"],
|
|
669
|
-
dependsOn: ["url"],
|
|
670
771
|
}),
|
|
671
772
|
url: core_1.Flags.string({
|
|
672
773
|
description: "The url",
|
|
@@ -689,9 +790,11 @@ Run.flags = {
|
|
|
689
790
|
"settings-path": core_1.Flags.string({
|
|
690
791
|
description: "Settings path",
|
|
691
792
|
}),
|
|
793
|
+
"local-test-nodes-path": core_1.Flags.string({
|
|
794
|
+
description: "Path to a YAML or JSON file with local test nodes configuration",
|
|
795
|
+
}),
|
|
692
796
|
"test-case-uuid": core_1.Flags.string({
|
|
693
797
|
description: "Test case uuid",
|
|
694
|
-
dependsOn: ["url"],
|
|
695
798
|
}),
|
|
696
799
|
"auto-create-ai-rules": core_1.Flags.boolean({
|
|
697
800
|
description: "Enable auto create AI based rules",
|
|
@@ -718,6 +821,9 @@ Run.flags = {
|
|
|
718
821
|
"chrome-extension": (0, chrome_extension_1.ChromeExtensionFlag)({
|
|
719
822
|
exclusive: ["file-path"],
|
|
720
823
|
}),
|
|
824
|
+
"xray-execution-key": core_1.Flags.string({
|
|
825
|
+
description: "Reuse an existing Xray Cloud Test Execution by key (e.g. XR-123) instead of creating a new one. Optional; leave blank to create a new execution.",
|
|
826
|
+
}),
|
|
721
827
|
};
|
|
722
828
|
Run.args = {
|
|
723
829
|
testSuiteId: core_1.Args.string({
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
declare const DEFAULT_PORT = 4723;
|
|
2
|
+
declare const DEFAULT_BASE_PATH = "/wd/hub";
|
|
3
|
+
declare const PID_DIR: string;
|
|
4
|
+
declare const PID_FILE: string;
|
|
5
|
+
declare const REQUIRED_DRIVERS: Array<{
|
|
6
|
+
name: string;
|
|
7
|
+
version: string;
|
|
8
|
+
}>;
|
|
9
|
+
export { DEFAULT_PORT, DEFAULT_BASE_PATH, REQUIRED_DRIVERS, PID_DIR, PID_FILE };
|
|
10
|
+
export declare function checkAppiumInstalled(): Promise<void>;
|
|
11
|
+
export type DriverStatus = {
|
|
12
|
+
installed: boolean;
|
|
13
|
+
version?: string;
|
|
14
|
+
upToDate: boolean;
|
|
15
|
+
};
|
|
16
|
+
export declare function getDriverStatus(driverName: string, requiredVersion: string): Promise<DriverStatus>;
|
|
17
|
+
export declare function ensureDriver(driverName: string, requiredVersion: string): Promise<string | null>;
|
|
18
|
+
export declare function isProcessRunning(pid: number): boolean;
|
|
19
|
+
export type StartResult = {
|
|
20
|
+
pid: number;
|
|
21
|
+
alreadyRunning: boolean;
|
|
22
|
+
};
|
|
23
|
+
export declare function getRunningAppiumPid(): Promise<number | null>;
|
|
24
|
+
export declare function startAppium(port?: number, logLevel?: string): Promise<StartResult>;
|
|
25
|
+
export declare function stopAppium(): Promise<number | null>;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.stopAppium = exports.startAppium = exports.getRunningAppiumPid = exports.isProcessRunning = exports.ensureDriver = exports.getDriverStatus = exports.checkAppiumInstalled = exports.PID_FILE = exports.PID_DIR = exports.REQUIRED_DRIVERS = exports.DEFAULT_BASE_PATH = exports.DEFAULT_PORT = void 0;
|
|
4
|
+
const childProcess = require("child_process");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const util = require("util");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const os = require("os");
|
|
9
|
+
const Fs = require("@supercharge/fs");
|
|
10
|
+
const execFile = util.promisify(childProcess.execFile);
|
|
11
|
+
const PROJECT_ROOT = path.resolve(__dirname, "..", "..");
|
|
12
|
+
const APPIUM_HOME = path.join(PROJECT_ROOT, ".appium");
|
|
13
|
+
const DEFAULT_PORT = 4723;
|
|
14
|
+
exports.DEFAULT_PORT = DEFAULT_PORT;
|
|
15
|
+
const DEFAULT_BASE_PATH = "/wd/hub";
|
|
16
|
+
exports.DEFAULT_BASE_PATH = DEFAULT_BASE_PATH;
|
|
17
|
+
const PID_DIR = path.join(os.homedir(), ".testrigor");
|
|
18
|
+
exports.PID_DIR = PID_DIR;
|
|
19
|
+
const PID_FILE = path.join(PID_DIR, "appium.pid");
|
|
20
|
+
exports.PID_FILE = PID_FILE;
|
|
21
|
+
const LOG_FILE = path.join(os.tmpdir(), "testrigor-appium.log");
|
|
22
|
+
const REQUIRED_DRIVERS = [
|
|
23
|
+
{ name: "uiautomator2", version: "4.2.3" },
|
|
24
|
+
{ name: "xcuitest", version: "9.2.4" },
|
|
25
|
+
];
|
|
26
|
+
exports.REQUIRED_DRIVERS = REQUIRED_DRIVERS;
|
|
27
|
+
function getAppiumBin() {
|
|
28
|
+
return path.resolve(PROJECT_ROOT, "node_modules", ".bin", "appium");
|
|
29
|
+
}
|
|
30
|
+
function getAppiumEnv() {
|
|
31
|
+
return {
|
|
32
|
+
...process.env,
|
|
33
|
+
APPIUM_HOME,
|
|
34
|
+
CHROMELABS_URL: "https://googlechromelabs.github.io",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
async function checkAppiumInstalled() {
|
|
38
|
+
const appiumBin = getAppiumBin();
|
|
39
|
+
if (!(await Fs.exists(appiumBin))) {
|
|
40
|
+
throw new Error(`Appium binary not found at ${appiumBin}. Try reinstalling testrigor-cli.`);
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
await execFile(appiumBin, ["--version"], { env: getAppiumEnv() });
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw new Error(`Appium binary at ${appiumBin} is not working. Try reinstalling testrigor-cli.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.checkAppiumInstalled = checkAppiumInstalled;
|
|
50
|
+
async function getDriverStatus(driverName, requiredVersion) {
|
|
51
|
+
const appiumBin = getAppiumBin();
|
|
52
|
+
try {
|
|
53
|
+
const { stdout } = await execFile(appiumBin, ["driver", "list", "--installed", "--json"], { encoding: "utf8", env: getAppiumEnv() });
|
|
54
|
+
const drivers = JSON.parse(stdout);
|
|
55
|
+
const info = drivers[driverName];
|
|
56
|
+
if (!info || !info.installed) {
|
|
57
|
+
return { installed: false, upToDate: false };
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
installed: true,
|
|
61
|
+
version: info.version,
|
|
62
|
+
upToDate: info.version === requiredVersion,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return { installed: false, upToDate: false };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.getDriverStatus = getDriverStatus;
|
|
70
|
+
async function ensureDriver(driverName, requiredVersion) {
|
|
71
|
+
const appiumBin = getAppiumBin();
|
|
72
|
+
const env = getAppiumEnv();
|
|
73
|
+
const status = await getDriverStatus(driverName, requiredVersion);
|
|
74
|
+
if (status.installed && status.upToDate) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
if (status.installed) {
|
|
79
|
+
await execFile(appiumBin, ["driver", "uninstall", driverName], {
|
|
80
|
+
encoding: "utf8",
|
|
81
|
+
env,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
await execFile(appiumBin, ["driver", "install", `${driverName}@${requiredVersion}`], { encoding: "utf8", env });
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
89
|
+
return raw.split("\n")[0].replace(/^Command failed:.*?Error:\s*/, "").trim();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
exports.ensureDriver = ensureDriver;
|
|
93
|
+
function isProcessRunning(pid) {
|
|
94
|
+
try {
|
|
95
|
+
process.kill(pid, 0);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exports.isProcessRunning = isProcessRunning;
|
|
103
|
+
function killSafe(pid) {
|
|
104
|
+
if (isProcessRunning(pid)) {
|
|
105
|
+
process.kill(pid);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function getRunningAppiumPid() {
|
|
109
|
+
if (!(await Fs.exists(PID_FILE))) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const existingPid = Number((await Fs.content(PID_FILE)).trim());
|
|
113
|
+
if (isProcessRunning(existingPid)) {
|
|
114
|
+
return existingPid;
|
|
115
|
+
}
|
|
116
|
+
await Fs.removeFile(PID_FILE);
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
exports.getRunningAppiumPid = getRunningAppiumPid;
|
|
120
|
+
async function healthCheck(port, retries = 15, interval = 2000) {
|
|
121
|
+
const url = `http://127.0.0.1:${port}/wd/hub/status`;
|
|
122
|
+
let lastError = "";
|
|
123
|
+
for (let i = 0; i < retries; i++) {
|
|
124
|
+
try {
|
|
125
|
+
const res = await fetch(url);
|
|
126
|
+
const body = await res.json();
|
|
127
|
+
if (body.value?.ready)
|
|
128
|
+
return;
|
|
129
|
+
lastError = "server responded but not ready";
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
lastError = err.message || String(err);
|
|
133
|
+
}
|
|
134
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
135
|
+
}
|
|
136
|
+
throw new Error(`Mobile server not ready after ${retries} attempts: ${lastError}`);
|
|
137
|
+
}
|
|
138
|
+
async function startAppium(port = DEFAULT_PORT, logLevel = "debug") {
|
|
139
|
+
const existingPid = await getRunningAppiumPid();
|
|
140
|
+
if (existingPid) {
|
|
141
|
+
return { pid: existingPid, alreadyRunning: true };
|
|
142
|
+
}
|
|
143
|
+
const appiumBin = getAppiumBin();
|
|
144
|
+
const logStream = fs.openSync(LOG_FILE, "a");
|
|
145
|
+
const appiumProcess = childProcess.spawn(appiumBin, [
|
|
146
|
+
"--port",
|
|
147
|
+
String(port),
|
|
148
|
+
"--base-path",
|
|
149
|
+
DEFAULT_BASE_PATH,
|
|
150
|
+
"--allow-insecure",
|
|
151
|
+
"chromedriver_autodownload",
|
|
152
|
+
"--log-level",
|
|
153
|
+
logLevel,
|
|
154
|
+
], {
|
|
155
|
+
detached: true,
|
|
156
|
+
stdio: ["ignore", logStream, logStream],
|
|
157
|
+
env: getAppiumEnv(),
|
|
158
|
+
});
|
|
159
|
+
const pid = appiumProcess.pid;
|
|
160
|
+
if (!pid) {
|
|
161
|
+
throw new Error("Failed to start Appium server: no PID returned");
|
|
162
|
+
}
|
|
163
|
+
appiumProcess.unref();
|
|
164
|
+
await Fs.ensureDir(PID_DIR);
|
|
165
|
+
await Fs.writeFile(PID_FILE, String(pid));
|
|
166
|
+
try {
|
|
167
|
+
await healthCheck(port);
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
killSafe(pid);
|
|
171
|
+
await Fs.removeFile(PID_FILE);
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
174
|
+
return { pid, alreadyRunning: false };
|
|
175
|
+
}
|
|
176
|
+
exports.startAppium = startAppium;
|
|
177
|
+
async function stopAppium() {
|
|
178
|
+
if (!(await Fs.exists(PID_FILE))) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
const pidContent = (await Fs.content(PID_FILE)).trim();
|
|
182
|
+
const pid = Number(pidContent);
|
|
183
|
+
if (!isNaN(pid) && isProcessRunning(pid)) {
|
|
184
|
+
process.kill(pid, "SIGTERM");
|
|
185
|
+
}
|
|
186
|
+
await Fs.removeFile(PID_FILE);
|
|
187
|
+
return pid;
|
|
188
|
+
}
|
|
189
|
+
exports.stopAppium = stopAppium;
|
package/dist/util/trtc.d.ts
CHANGED
package/dist/util/trtc.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.download = void 0;
|
|
3
|
+
exports.download = exports.BINARY_NAME = void 0;
|
|
4
4
|
const Fs = require("@supercharge/fs");
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const node_fetch_commonjs_1 = require("node-fetch-commonjs");
|
|
@@ -25,6 +25,10 @@ const ARCHITECTURE_MAP = {
|
|
|
25
25
|
arm64: "arm64",
|
|
26
26
|
};
|
|
27
27
|
const VERSION = "2.3.15";
|
|
28
|
+
const WIN32_PLATFORM = "win32";
|
|
29
|
+
const WIN32_BINARY = "trtc.exe";
|
|
30
|
+
const UNIX_BINARY = "trtc";
|
|
31
|
+
exports.BINARY_NAME = os.platform() === WIN32_PLATFORM ? WIN32_BINARY : UNIX_BINARY;
|
|
28
32
|
function getFolderName() {
|
|
29
33
|
const platform = os.platform();
|
|
30
34
|
const architecture = os.arch();
|
package/dist/util/tunnel.d.ts
CHANGED
package/dist/util/tunnel.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const Fs = require("@supercharge/fs");
|
|
4
4
|
const childProcess = require("child_process");
|
|
5
|
-
const os = require("os");
|
|
6
5
|
const path = require("path");
|
|
7
6
|
const trtc_1 = require("./trtc");
|
|
8
7
|
const utils_1 = require("./utils");
|
|
@@ -11,24 +10,25 @@ function randomPort(min, max) {
|
|
|
11
10
|
}
|
|
12
11
|
async function updateConfig(dir, port) {
|
|
13
12
|
const parentDir = path.dirname(dir);
|
|
14
|
-
const configPath =
|
|
13
|
+
const configPath = path.join(parentDir, "config.toml");
|
|
15
14
|
const newConfigContent = `[[proxies]]
|
|
16
15
|
name = "http_proxy_${port}"
|
|
17
16
|
type = "tcp"
|
|
18
17
|
remotePort = ${port}
|
|
19
18
|
[proxies.plugin]
|
|
20
19
|
type = "http_proxy"`;
|
|
21
|
-
|
|
20
|
+
await Fs.writeFile(configPath, newConfigContent);
|
|
21
|
+
return configPath;
|
|
22
22
|
}
|
|
23
|
-
async function startTrtc(trtc, command) {
|
|
23
|
+
async function startTrtc(trtc, command, configPath) {
|
|
24
24
|
return new Promise((resolve, reject) => {
|
|
25
25
|
const binaryPath = path.join(path.dirname(trtc), command);
|
|
26
|
-
const workingDir = path.dirname(trtc);
|
|
26
|
+
const workingDir = path.dirname(trtc);
|
|
27
27
|
if (!Fs.pathExistsSync(binaryPath)) {
|
|
28
28
|
reject(new Error(`Binary not found at ${binaryPath}`));
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
|
-
const tunnelProcess = childProcess.spawn(binaryPath, [], {
|
|
31
|
+
const tunnelProcess = childProcess.spawn(binaryPath, ["-c", configPath], {
|
|
32
32
|
cwd: workingDir,
|
|
33
33
|
});
|
|
34
34
|
tunnelProcess.stdout.on("data", (data) => {
|
|
@@ -68,13 +68,13 @@ class Tunnel {
|
|
|
68
68
|
if (this.binaryPath == null) {
|
|
69
69
|
throw new Error("Could not download the trtc binary");
|
|
70
70
|
}
|
|
71
|
-
const command =
|
|
71
|
+
const command = trtc_1.BINARY_NAME;
|
|
72
72
|
let maxTries = 40;
|
|
73
73
|
while (maxTries > 0) {
|
|
74
74
|
try {
|
|
75
75
|
this.port = randomPort(minPort, maxPort);
|
|
76
|
-
await updateConfig(this.binaryPath, this.port);
|
|
77
|
-
this.process = await startTrtc(this.binaryPath, command);
|
|
76
|
+
this.configPath = await updateConfig(this.binaryPath, this.port);
|
|
77
|
+
this.process = await startTrtc(this.binaryPath, command, this.configPath);
|
|
78
78
|
break;
|
|
79
79
|
}
|
|
80
80
|
catch (e) {
|
package/oclif.manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.0.
|
|
2
|
+
"version": "0.0.44-beta",
|
|
3
3
|
"commands": {
|
|
4
4
|
"authenticate": {
|
|
5
5
|
"id": "authenticate",
|
|
@@ -147,11 +147,8 @@
|
|
|
147
147
|
"localhost": {
|
|
148
148
|
"name": "localhost",
|
|
149
149
|
"type": "boolean",
|
|
150
|
-
"description": "Localhost run",
|
|
150
|
+
"description": "Localhost run (auto-detects web vs mobile)",
|
|
151
151
|
"allowNo": false,
|
|
152
|
-
"dependsOn": [
|
|
153
|
-
"url"
|
|
154
|
-
],
|
|
155
152
|
"aliases": [
|
|
156
153
|
"tunnel"
|
|
157
154
|
]
|
|
@@ -198,14 +195,17 @@
|
|
|
198
195
|
"description": "Settings path",
|
|
199
196
|
"multiple": false
|
|
200
197
|
},
|
|
198
|
+
"local-test-nodes-path": {
|
|
199
|
+
"name": "local-test-nodes-path",
|
|
200
|
+
"type": "option",
|
|
201
|
+
"description": "Path to a YAML or JSON file with local test nodes configuration",
|
|
202
|
+
"multiple": false
|
|
203
|
+
},
|
|
201
204
|
"test-case-uuid": {
|
|
202
205
|
"name": "test-case-uuid",
|
|
203
206
|
"type": "option",
|
|
204
207
|
"description": "Test case uuid",
|
|
205
|
-
"multiple": false
|
|
206
|
-
"dependsOn": [
|
|
207
|
-
"url"
|
|
208
|
-
]
|
|
208
|
+
"multiple": false
|
|
209
209
|
},
|
|
210
210
|
"auto-create-ai-rules": {
|
|
211
211
|
"name": "auto-create-ai-rules",
|
|
@@ -251,6 +251,12 @@
|
|
|
251
251
|
"exclusive": [
|
|
252
252
|
"file-path"
|
|
253
253
|
]
|
|
254
|
+
},
|
|
255
|
+
"xray-execution-key": {
|
|
256
|
+
"name": "xray-execution-key",
|
|
257
|
+
"type": "option",
|
|
258
|
+
"description": "Reuse an existing Xray Cloud Test Execution by key (e.g. XR-123) instead of creating a new one. Optional; leave blank to create a new execution.",
|
|
259
|
+
"multiple": false
|
|
254
260
|
}
|
|
255
261
|
},
|
|
256
262
|
"args": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testrigor/testrigor-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.44-beta",
|
|
4
4
|
"description": "TestRigor Command Line",
|
|
5
5
|
"author": "Ivo Parun Rua @parunruaivo",
|
|
6
6
|
"bin": {
|
|
@@ -9,10 +9,14 @@
|
|
|
9
9
|
"homepage": "https://github.com/TestRigor/testrigor-cli",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"main": "dist/index.ts",
|
|
12
|
-
"repository":
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/TestRigor/testrigor-cli.git"
|
|
15
|
+
},
|
|
13
16
|
"files": [
|
|
14
17
|
"/bin",
|
|
15
18
|
"/dist",
|
|
19
|
+
"/.appium",
|
|
16
20
|
"/npm-shrinkwrap.json",
|
|
17
21
|
"oclif.manifest.json"
|
|
18
22
|
],
|
|
@@ -21,6 +25,7 @@
|
|
|
21
25
|
"@oclif/plugin-help": "^5",
|
|
22
26
|
"@oclif/plugin-plugins": "^3.2.0",
|
|
23
27
|
"@supercharge/fs": "^3.4.0",
|
|
28
|
+
"appium": "^2.18",
|
|
24
29
|
"form-data": "^4.0.1",
|
|
25
30
|
"glob": "^10.3.10",
|
|
26
31
|
"gunzip-maybe": "^1.4.2",
|
|
@@ -85,7 +90,7 @@
|
|
|
85
90
|
"prepack": "npm run build && oclif manifest && oclif readme",
|
|
86
91
|
"test": "mocha --forbid-only \"test/**/*.ts\"",
|
|
87
92
|
"version": "oclif readme && git add README.md",
|
|
88
|
-
"tarballs": "npm run prepack && oclif pack tarballs"
|
|
93
|
+
"tarballs": "npm run prepack && oclif pack tarballs --targets linux-x64,linux-arm64,darwin-x64,darwin-arm64,win32-x64"
|
|
89
94
|
},
|
|
90
95
|
"engines": {
|
|
91
96
|
"node": ">=18.0.0"
|