@testrigor/testrigor-cli 2.0.1 → 2.0.2
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 +2 -1
- package/appium-server/package-lock.json +4148 -0
- package/appium-server/package.json +11 -0
- package/bin/dev +2 -8
- package/bin/run +1 -3
- package/dist/command-base.d.ts +4 -4
- package/dist/commands/authenticate.d.ts +3 -5
- package/dist/commands/authenticate.js +14 -12
- package/dist/commands/test-suite/config.d.ts +3 -3
- package/dist/commands/test-suite/config.js +12 -12
- package/dist/commands/test-suite/run.d.ts +23 -23
- package/dist/commands/test-suite/run.js +6 -2
- package/dist/commands/test-suite/validate-syntax.d.ts +6 -6
- package/dist/commands/test-suite/validate-syntax.js +4 -4
- package/dist/flags/chrome-extension.d.ts +5 -2
- package/dist/util/appium.d.ts +9 -0
- package/dist/util/appium.js +98 -20
- package/npm-shrinkwrap.json +1054 -10227
- package/oclif.manifest.json +5 -1
- package/package.json +12 -9
- package/scripts/check-node.js +22 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "testrigor-cli-appium-server",
|
|
3
|
+
"private": true,
|
|
4
|
+
"description": "Appium server installed by the testRigor CLI on the first mobile run. See src/util/appium.ts.",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"appium": "3.8.0"
|
|
7
|
+
},
|
|
8
|
+
"overrides": {
|
|
9
|
+
"shell-quote": "^1.12.0"
|
|
10
|
+
}
|
|
11
|
+
}
|
package/bin/dev
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const oclif = require('@oclif/core')
|
|
4
|
-
|
|
5
3
|
const path = require('path')
|
|
6
4
|
const project = path.join(__dirname, '..', 'tsconfig.json')
|
|
7
5
|
|
|
@@ -10,9 +8,5 @@ process.env.NODE_ENV = 'development'
|
|
|
10
8
|
|
|
11
9
|
require('ts-node').register({project})
|
|
12
10
|
|
|
13
|
-
//
|
|
14
|
-
oclif.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// Start the CLI
|
|
18
|
-
oclif.run().then(oclif.flush).catch(oclif.Errors.handle)
|
|
11
|
+
// development: true shows stack traces and loads commands from src/
|
|
12
|
+
require('@oclif/core').execute({development: true, dir: __dirname})
|
package/bin/run
CHANGED
package/dist/command-base.d.ts
CHANGED
|
@@ -4,10 +4,10 @@ export type CommandFlags<T extends typeof Command> = Interfaces.InferredFlags<(t
|
|
|
4
4
|
export type Args<T extends typeof Command> = Interfaces.InferredArgs<T["args"]>;
|
|
5
5
|
export declare abstract class CommandBase<T extends typeof Command> extends Command {
|
|
6
6
|
static baseFlags: {
|
|
7
|
-
"auth-token": Interfaces.OptionFlag<string | undefined,
|
|
8
|
-
token: Interfaces.OptionFlag<string | undefined,
|
|
9
|
-
"min-port": Interfaces.OptionFlag<number,
|
|
10
|
-
"max-port": Interfaces.OptionFlag<number,
|
|
7
|
+
"auth-token": Interfaces.OptionFlag<string | undefined, Interfaces.CustomOptions>;
|
|
8
|
+
token: Interfaces.OptionFlag<string | undefined, Interfaces.CustomOptions>;
|
|
9
|
+
"min-port": Interfaces.OptionFlag<number, Interfaces.CustomOptions>;
|
|
10
|
+
"max-port": Interfaces.OptionFlag<number, Interfaces.CustomOptions>;
|
|
11
11
|
verbose: Interfaces.BooleanFlag<boolean>;
|
|
12
12
|
};
|
|
13
13
|
protected flags: CommandFlags<T>;
|
|
@@ -3,17 +3,15 @@ export default class Authenticate extends Command {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
-
token: import("@oclif/core/
|
|
6
|
+
token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
7
|
};
|
|
8
8
|
run(): Promise<void>;
|
|
9
9
|
/**
|
|
10
10
|
* Where the token comes from, in order: the --token flag, an interactive
|
|
11
11
|
* hidden prompt, or piped stdin.
|
|
12
12
|
*
|
|
13
|
-
* The hidden prompt is only usable on a TTY
|
|
14
|
-
*
|
|
15
|
-
* dash (the Debian and Ubuntu default) with "read: Illegal option -s" and
|
|
16
|
-
* then silently skips the setup.
|
|
13
|
+
* The hidden prompt is only usable on a TTY: without one there is nothing
|
|
14
|
+
* to read keystrokes from, so piped stdin is tried instead.
|
|
17
15
|
*/
|
|
18
16
|
private resolveToken;
|
|
19
17
|
private readStdin;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
3
4
|
const core_1 = require("@oclif/core");
|
|
4
5
|
const core_2 = require("@oclif/core");
|
|
6
|
+
const password_1 = tslib_1.__importDefault(require("@inquirer/password"));
|
|
5
7
|
const config_1 = require("../util/config");
|
|
6
8
|
// Time to wait for a token on a piped stdin before giving up. Stdin that is
|
|
7
9
|
// neither a TTY nor a closed/redirected stream would otherwise hang forever.
|
|
@@ -20,21 +22,21 @@ class Authenticate extends core_2.Command {
|
|
|
20
22
|
if (config.authToken) {
|
|
21
23
|
delete config.authToken;
|
|
22
24
|
await (0, config_1.writeConfig)(config);
|
|
23
|
-
core_1.ux.
|
|
25
|
+
core_1.ux.stdout("Authentication token removed");
|
|
24
26
|
}
|
|
25
27
|
else {
|
|
26
|
-
core_1.ux.
|
|
28
|
+
core_1.ux.stdout("Authentication token setup skipped");
|
|
27
29
|
}
|
|
28
30
|
return;
|
|
29
31
|
}
|
|
30
32
|
config.authToken = token.trim();
|
|
31
33
|
await (0, config_1.writeConfig)(config);
|
|
32
|
-
core_1.ux.
|
|
34
|
+
core_1.ux.stdout("Authentication token successfully saved");
|
|
33
35
|
if (config.defaultTestSuiteId) {
|
|
34
|
-
core_1.ux.
|
|
36
|
+
core_1.ux.stdout("You can now use 'testrigor run' to run your default test suite");
|
|
35
37
|
}
|
|
36
38
|
else {
|
|
37
|
-
core_1.ux.
|
|
39
|
+
core_1.ux.stdout("Use 'testrigor test-suite config --default TEST_SUITE_ID' to set your default test suite ID, then use 'testrigor run'");
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
catch (error) {
|
|
@@ -45,19 +47,19 @@ class Authenticate extends core_2.Command {
|
|
|
45
47
|
* Where the token comes from, in order: the --token flag, an interactive
|
|
46
48
|
* hidden prompt, or piped stdin.
|
|
47
49
|
*
|
|
48
|
-
* The hidden prompt is only usable on a TTY
|
|
49
|
-
*
|
|
50
|
-
* dash (the Debian and Ubuntu default) with "read: Illegal option -s" and
|
|
51
|
-
* then silently skips the setup.
|
|
50
|
+
* The hidden prompt is only usable on a TTY: without one there is nothing
|
|
51
|
+
* to read keystrokes from, so piped stdin is tried instead.
|
|
52
52
|
*/
|
|
53
53
|
async resolveToken(flagToken, hasExistingToken) {
|
|
54
54
|
if (flagToken !== undefined) {
|
|
55
55
|
return flagToken;
|
|
56
56
|
}
|
|
57
57
|
if (process.stdin.isTTY) {
|
|
58
|
-
return
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
return (0, password_1.default)({
|
|
59
|
+
message: hasExistingToken
|
|
60
|
+
? "Enter your testRigor API token (press enter to remove)"
|
|
61
|
+
: "Enter your testRigor API token (press enter to skip)",
|
|
62
|
+
});
|
|
61
63
|
}
|
|
62
64
|
const piped = await this.readStdin();
|
|
63
65
|
if (piped) {
|
|
@@ -3,9 +3,9 @@ export default class Config extends Command {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
-
default: import("@oclif/core/
|
|
7
|
-
delete: import("@oclif/core/
|
|
8
|
-
show: import("@oclif/core/
|
|
6
|
+
default: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
delete: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
show: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
9
|
};
|
|
10
10
|
run(): Promise<void>;
|
|
11
11
|
private setDefaultTestSuite;
|
|
@@ -32,45 +32,45 @@ class Config extends core_1.Command {
|
|
|
32
32
|
const config = await (0, config_1.readConfig)();
|
|
33
33
|
config.defaultTestSuiteId = testSuiteId.trim();
|
|
34
34
|
await (0, config_1.writeConfig)(config);
|
|
35
|
-
core_1.ux.
|
|
35
|
+
core_1.ux.stdout(`Default test suite ID set to: ${config.defaultTestSuiteId}`);
|
|
36
36
|
if (config.authToken) {
|
|
37
|
-
core_1.ux.
|
|
37
|
+
core_1.ux.stdout("You can now use 'testrigor run' to run your default test suite");
|
|
38
38
|
}
|
|
39
39
|
else {
|
|
40
|
-
core_1.ux.
|
|
40
|
+
core_1.ux.stdout("Use 'testrigor authenticate' to set your authentication token");
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
async deleteDefaultTestSuite() {
|
|
44
44
|
const config = await (0, config_1.readConfig)();
|
|
45
45
|
if (!config.defaultTestSuiteId) {
|
|
46
|
-
core_1.ux.
|
|
46
|
+
core_1.ux.stdout("No default test suite ID is currently set");
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
49
|
delete config.defaultTestSuiteId;
|
|
50
50
|
await (0, config_1.writeConfig)(config);
|
|
51
|
-
core_1.ux.
|
|
51
|
+
core_1.ux.stdout("Default test suite ID removed");
|
|
52
52
|
if (config.authToken) {
|
|
53
|
-
core_1.ux.
|
|
53
|
+
core_1.ux.stdout("Authentication token preserved");
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
async showConfiguration() {
|
|
57
57
|
const config = await (0, config_1.readConfig)();
|
|
58
58
|
if (Object.keys(config).length === 0) {
|
|
59
|
-
core_1.ux.
|
|
59
|
+
core_1.ux.stdout("No configuration found");
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
|
-
core_1.ux.
|
|
62
|
+
core_1.ux.stdout("Current testRigor configuration:");
|
|
63
63
|
if (config.defaultTestSuiteId) {
|
|
64
|
-
core_1.ux.
|
|
64
|
+
core_1.ux.stdout(` Default test suite ID: ${config.defaultTestSuiteId}`);
|
|
65
65
|
}
|
|
66
66
|
else {
|
|
67
|
-
core_1.ux.
|
|
67
|
+
core_1.ux.stdout(" Default test suite ID: not set");
|
|
68
68
|
}
|
|
69
69
|
if (config.authToken) {
|
|
70
|
-
core_1.ux.
|
|
70
|
+
core_1.ux.stdout(" Authentication token: configured");
|
|
71
71
|
}
|
|
72
72
|
else {
|
|
73
|
-
core_1.ux.
|
|
73
|
+
core_1.ux.stdout(" Authentication token: not set");
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
}
|
|
@@ -6,33 +6,33 @@ export default class Run extends CommandBase<typeof Run> {
|
|
|
6
6
|
private TESTRIGOR_API_URL;
|
|
7
7
|
private TESTRIGOR_API2_URL;
|
|
8
8
|
static flags: {
|
|
9
|
-
async: import("@oclif/core/
|
|
10
|
-
branch: import("@oclif/core/
|
|
11
|
-
commit: import("@oclif/core/
|
|
12
|
-
localhost: import("@oclif/core/
|
|
13
|
-
url: import("@oclif/core/
|
|
14
|
-
"file-path": import("@oclif/core/
|
|
15
|
-
"junit-report-save-path": import("@oclif/core/
|
|
16
|
-
"variables-path": import("@oclif/core/
|
|
17
|
-
"test-cases-path": import("@oclif/core/
|
|
18
|
-
"rules-path": import("@oclif/core/
|
|
19
|
-
"settings-path": import("@oclif/core/
|
|
20
|
-
"local-test-nodes-path": import("@oclif/core/
|
|
21
|
-
"test-case-uuid": import("@oclif/core/
|
|
22
|
-
"auto-create-ai-rules": import("@oclif/core/
|
|
23
|
-
"explicit-mutations": import("@oclif/core/
|
|
24
|
-
labels: import("@oclif/core/
|
|
25
|
-
"excluded-labels": import("@oclif/core/
|
|
26
|
-
"base-url-web": import("@oclif/core/
|
|
27
|
-
"base-url-api": import("@oclif/core/
|
|
28
|
-
"chrome-extension": import("@oclif/core/
|
|
9
|
+
async: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
commit: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
localhost: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
url: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
"file-path": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
"junit-report-save-path": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
"variables-path": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
"test-cases-path": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
|
+
"rules-path": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
|
+
"settings-path": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
20
|
+
"local-test-nodes-path": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
21
|
+
"test-case-uuid": import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
22
|
+
"auto-create-ai-rules": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
23
|
+
"explicit-mutations": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
24
|
+
labels: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
25
|
+
"excluded-labels": import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
26
|
+
"base-url-web": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
27
|
+
"base-url-api": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
28
|
+
"chrome-extension": import("@oclif/core/interfaces").OptionFlag<{
|
|
29
29
|
name: string;
|
|
30
30
|
file: string;
|
|
31
|
-
} | undefined, import("@oclif/core/
|
|
32
|
-
"xray-execution-key": import("@oclif/core/
|
|
31
|
+
}[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
32
|
+
"xray-execution-key": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
33
33
|
};
|
|
34
34
|
static args: {
|
|
35
|
-
testSuiteId: import("@oclif/core/
|
|
35
|
+
testSuiteId: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
36
36
|
};
|
|
37
37
|
private initializeUrls;
|
|
38
38
|
run(): Promise<void>;
|
|
@@ -194,7 +194,7 @@ class Run extends command_base_1.CommandBase {
|
|
|
194
194
|
}
|
|
195
195
|
if (filePath) {
|
|
196
196
|
if (url) {
|
|
197
|
-
core_1.ux.
|
|
197
|
+
core_1.ux.stdout("WARNING: File path will be ignored because --url is used");
|
|
198
198
|
delete flags["file-path"];
|
|
199
199
|
}
|
|
200
200
|
else {
|
|
@@ -231,8 +231,12 @@ class Run extends command_base_1.CommandBase {
|
|
|
231
231
|
return task.newListr([
|
|
232
232
|
{
|
|
233
233
|
task: async (_ctx, subtask) => {
|
|
234
|
+
if (!(await (0, appium_1.isAppiumInstalled)())) {
|
|
235
|
+
subtask.title = `Installing mobile server (appium@${(0, appium_1.getAppiumVersion)()}, first run only)`;
|
|
236
|
+
await (0, appium_1.installAppium)();
|
|
237
|
+
}
|
|
234
238
|
await (0, appium_1.checkAppiumInstalled)();
|
|
235
|
-
subtask.title = "Mobile server
|
|
239
|
+
subtask.title = "Mobile server installed";
|
|
236
240
|
},
|
|
237
241
|
rendererOptions: { timer: listr2_1.PRESET_TIMER },
|
|
238
242
|
},
|
|
@@ -4,14 +4,14 @@ export default class ValidateSyntax extends CommandBase<typeof ValidateSyntax> {
|
|
|
4
4
|
static examples: string[];
|
|
5
5
|
private TESTRIGOR_API2_URL;
|
|
6
6
|
static flags: {
|
|
7
|
-
steps: import("@oclif/core/
|
|
8
|
-
"test-cases-path": import("@oclif/core/
|
|
9
|
-
"base-url-api": import("@oclif/core/
|
|
10
|
-
"rules-path": import("@oclif/core/
|
|
11
|
-
"explicit-mutations": import("@oclif/core/
|
|
7
|
+
steps: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
"test-cases-path": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
"base-url-api": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
"rules-path": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
"explicit-mutations": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
12
|
};
|
|
13
13
|
static args: {
|
|
14
|
-
testSuiteId: import("@oclif/core/
|
|
14
|
+
testSuiteId: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
15
15
|
};
|
|
16
16
|
run(): Promise<void>;
|
|
17
17
|
private resolveStepsToValidate;
|
|
@@ -42,20 +42,20 @@ class ValidateSyntax extends command_base_1.CommandBase {
|
|
|
42
42
|
const response = await this.callValidateSyntax(testSuiteId, testCase.customSteps, token, rules, explicitMutations);
|
|
43
43
|
if (response.data && response.data.length > 0) {
|
|
44
44
|
const parserErrors = response.data;
|
|
45
|
-
core_2.ux.
|
|
45
|
+
core_2.ux.stdout(`${testCase.description}: invalid`);
|
|
46
46
|
for (const parserError of parserErrors) {
|
|
47
|
-
core_2.ux.
|
|
47
|
+
core_2.ux.stdout(` ${parserError.command} - ${parserError.errorMessage}`);
|
|
48
48
|
}
|
|
49
49
|
hasAnyInvalidTestCases = true;
|
|
50
50
|
}
|
|
51
51
|
else {
|
|
52
|
-
core_2.ux.
|
|
52
|
+
core_2.ux.stdout(`${testCase.description}: valid`);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
if (hasAnyInvalidTestCases) {
|
|
56
56
|
this.exit(1);
|
|
57
57
|
}
|
|
58
|
-
core_2.ux.
|
|
58
|
+
core_2.ux.stdout("All provided steps are valid.");
|
|
59
59
|
this.exit(0);
|
|
60
60
|
}
|
|
61
61
|
async callValidateSyntax(testSuiteId, steps, token, rules, explicitMutations) {
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
export declare const ChromeExtensionFlag: import("@oclif/core/
|
|
1
|
+
export declare const ChromeExtensionFlag: import("@oclif/core/interfaces").FlagDefinition<{
|
|
2
2
|
name: string;
|
|
3
3
|
file: string;
|
|
4
|
-
},
|
|
4
|
+
}, import("@oclif/core/interfaces").CustomOptions, {
|
|
5
|
+
multiple: true;
|
|
6
|
+
requiredOrDefaulted: false;
|
|
7
|
+
}>;
|
package/dist/util/appium.d.ts
CHANGED
|
@@ -7,6 +7,15 @@ declare const REQUIRED_DRIVERS: Array<{
|
|
|
7
7
|
version: string;
|
|
8
8
|
}>;
|
|
9
9
|
export { DEFAULT_PORT, DEFAULT_BASE_PATH, REQUIRED_DRIVERS, PID_DIR, PID_FILE };
|
|
10
|
+
export declare function getAppiumVersion(): string;
|
|
11
|
+
export declare function isAppiumInstalled(): Promise<boolean>;
|
|
12
|
+
/**
|
|
13
|
+
* Installs the pinned Appium with `npm ci` from the lockfile in appium-server/.
|
|
14
|
+
* The install runs in a staging directory that is renamed into place, so an
|
|
15
|
+
* interrupted install never looks complete and two concurrent runs do not
|
|
16
|
+
* install into the same directory.
|
|
17
|
+
*/
|
|
18
|
+
export declare function installAppium(): Promise<void>;
|
|
10
19
|
export declare function checkAppiumInstalled(): Promise<void>;
|
|
11
20
|
export type DriverStatus = {
|
|
12
21
|
installed: boolean;
|
package/dist/util/appium.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PID_FILE = exports.PID_DIR = exports.REQUIRED_DRIVERS = exports.DEFAULT_BASE_PATH = exports.DEFAULT_PORT = void 0;
|
|
4
|
+
exports.getAppiumVersion = getAppiumVersion;
|
|
5
|
+
exports.isAppiumInstalled = isAppiumInstalled;
|
|
6
|
+
exports.installAppium = installAppium;
|
|
4
7
|
exports.checkAppiumInstalled = checkAppiumInstalled;
|
|
5
8
|
exports.getDriverStatus = getDriverStatus;
|
|
6
9
|
exports.ensureDriver = ensureDriver;
|
|
@@ -36,8 +39,28 @@ const REQUIRED_DRIVERS = [
|
|
|
36
39
|
{ name: "xcuitest", version: "10.43.1" },
|
|
37
40
|
];
|
|
38
41
|
exports.REQUIRED_DRIVERS = REQUIRED_DRIVERS;
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
// Appium is not a dependency of the CLI: its tree pins a vulnerable
|
|
43
|
+
// shell-quote, and npm re-resolves anything forced through our published
|
|
44
|
+
// shrinkwrap to satisfy that pin. appium-server/ instead ships a lockfile with
|
|
45
|
+
// an override, installed on the first mobile run under the CLI home, one
|
|
46
|
+
// directory per Appium version.
|
|
47
|
+
const APPIUM_SERVER_SOURCE = path.join(PROJECT_ROOT, "appium-server");
|
|
48
|
+
let appiumVersion;
|
|
49
|
+
function getAppiumVersion() {
|
|
50
|
+
if (!appiumVersion) {
|
|
51
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(APPIUM_SERVER_SOURCE, "package.json"), "utf8"));
|
|
52
|
+
appiumVersion = pkg.dependencies.appium;
|
|
53
|
+
}
|
|
54
|
+
return appiumVersion;
|
|
55
|
+
}
|
|
56
|
+
function getAppiumInstallDir() {
|
|
57
|
+
return path.join((0, utils_1.getHomeDir)(), "appium-server", getAppiumVersion());
|
|
58
|
+
}
|
|
59
|
+
// Run Appium's entry script with the Node running the CLI rather than through
|
|
60
|
+
// node_modules/.bin, whose shims differ per platform (`appium.cmd` on Windows).
|
|
61
|
+
function getAppiumCommand(args) {
|
|
62
|
+
const entry = path.join(getAppiumInstallDir(), "node_modules", "appium", "index.js");
|
|
63
|
+
return [process.execPath, [entry, ...args]];
|
|
41
64
|
}
|
|
42
65
|
function getAppiumEnv() {
|
|
43
66
|
return {
|
|
@@ -46,6 +69,13 @@ function getAppiumEnv() {
|
|
|
46
69
|
CHROMELABS_URL: "https://googlechromelabs.github.io",
|
|
47
70
|
};
|
|
48
71
|
}
|
|
72
|
+
function runAppium(args) {
|
|
73
|
+
const [command, commandArgs] = getAppiumCommand(args);
|
|
74
|
+
return execFile(command, commandArgs, {
|
|
75
|
+
encoding: "utf8",
|
|
76
|
+
env: getAppiumEnv(),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
49
79
|
/**
|
|
50
80
|
* Appium refuses to start unless APPIUM_HOME already exists, so create it
|
|
51
81
|
* before any invocation and surface a permission problem as itself rather
|
|
@@ -61,25 +91,78 @@ async function ensureAppiumHome() {
|
|
|
61
91
|
"Check that you own that directory, or point APPIUM_HOME at a writable one.");
|
|
62
92
|
}
|
|
63
93
|
}
|
|
94
|
+
async function isAppiumInstalled() {
|
|
95
|
+
try {
|
|
96
|
+
const pkg = JSON.parse(await fs.promises.readFile(path.join(getAppiumInstallDir(), "node_modules", "appium", "package.json"), "utf8"));
|
|
97
|
+
return pkg.version === getAppiumVersion();
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Installs the pinned Appium with `npm ci` from the lockfile in appium-server/.
|
|
105
|
+
* The install runs in a staging directory that is renamed into place, so an
|
|
106
|
+
* interrupted install never looks complete and two concurrent runs do not
|
|
107
|
+
* install into the same directory.
|
|
108
|
+
*/
|
|
109
|
+
async function installAppium() {
|
|
110
|
+
const target = getAppiumInstallDir();
|
|
111
|
+
await fs.promises.mkdir(path.dirname(target), { recursive: true });
|
|
112
|
+
const staging = await fs.promises.mkdtemp(`${target}.tmp-`);
|
|
113
|
+
for (const file of ["package.json", "package-lock.json"]) {
|
|
114
|
+
await fs.promises.copyFile(path.join(APPIUM_SERVER_SOURCE, file), path.join(staging, file));
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
// npm is a .cmd on Windows, which Node only spawns through a shell. The
|
|
118
|
+
// arguments are constants, so the shell adds no injection surface.
|
|
119
|
+
await execFile("npm", ["ci", "--omit=dev", "--no-audit", "--no-fund"], {
|
|
120
|
+
cwd: staging,
|
|
121
|
+
shell: process.platform === "win32",
|
|
122
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
await fs.promises.rm(staging, { recursive: true, force: true });
|
|
127
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
128
|
+
throw new Error(`Could not install the mobile server (appium@${getAppiumVersion()}) into ${target}. ` +
|
|
129
|
+
`npm must be installed and able to reach the registry.\n${detail}`);
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
// A damaged install (e.g. files deleted by hand) would block the rename.
|
|
133
|
+
if (!(await isAppiumInstalled())) {
|
|
134
|
+
await fs.promises.rm(target, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
await fs.promises.rename(staging, target);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
await fs.promises.rm(staging, { recursive: true, force: true });
|
|
140
|
+
// Another run finished installing first.
|
|
141
|
+
if (!(await isAppiumInstalled()))
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
64
145
|
async function checkAppiumInstalled() {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
throw new Error(`Appium binary not found at ${appiumBin}. ` +
|
|
68
|
-
"Reinstall the CLI with: npm install -g @testrigor/testrigor-cli");
|
|
146
|
+
if (!(await isAppiumInstalled())) {
|
|
147
|
+
throw new Error(`The mobile server (appium@${getAppiumVersion()}) is not installed in ${getAppiumInstallDir()}.`);
|
|
69
148
|
}
|
|
70
149
|
await ensureAppiumHome();
|
|
71
150
|
try {
|
|
72
|
-
await
|
|
151
|
+
await runAppium(["--version"]);
|
|
73
152
|
}
|
|
74
153
|
catch (error) {
|
|
75
154
|
const detail = error instanceof Error ? error.message : String(error);
|
|
76
|
-
throw new Error(`
|
|
155
|
+
throw new Error(`The mobile server in ${getAppiumInstallDir()} failed to run (APPIUM_HOME=${APPIUM_HOME}).\n${detail}`);
|
|
77
156
|
}
|
|
78
157
|
}
|
|
79
158
|
async function getDriverStatus(driverName, requiredVersion) {
|
|
80
|
-
const appiumBin = getAppiumBin();
|
|
81
159
|
try {
|
|
82
|
-
const { stdout } = await
|
|
160
|
+
const { stdout } = await runAppium([
|
|
161
|
+
"driver",
|
|
162
|
+
"list",
|
|
163
|
+
"--installed",
|
|
164
|
+
"--json",
|
|
165
|
+
]);
|
|
83
166
|
const drivers = JSON.parse(stdout);
|
|
84
167
|
const info = drivers[driverName];
|
|
85
168
|
if (!info || !info.installed) {
|
|
@@ -96,20 +179,15 @@ async function getDriverStatus(driverName, requiredVersion) {
|
|
|
96
179
|
}
|
|
97
180
|
}
|
|
98
181
|
async function ensureDriver(driverName, requiredVersion) {
|
|
99
|
-
const appiumBin = getAppiumBin();
|
|
100
|
-
const env = getAppiumEnv();
|
|
101
182
|
const status = await getDriverStatus(driverName, requiredVersion);
|
|
102
183
|
if (status.installed && status.upToDate) {
|
|
103
184
|
return null;
|
|
104
185
|
}
|
|
105
186
|
try {
|
|
106
187
|
if (status.installed) {
|
|
107
|
-
await
|
|
108
|
-
encoding: "utf8",
|
|
109
|
-
env,
|
|
110
|
-
});
|
|
188
|
+
await runAppium(["driver", "uninstall", driverName]);
|
|
111
189
|
}
|
|
112
|
-
await
|
|
190
|
+
await runAppium(["driver", "install", `${driverName}@${requiredVersion}`]);
|
|
113
191
|
return null;
|
|
114
192
|
}
|
|
115
193
|
catch (error) {
|
|
@@ -198,11 +276,10 @@ async function startAppium(port = DEFAULT_PORT, logLevel = "debug") {
|
|
|
198
276
|
if (existingPid) {
|
|
199
277
|
return { pid: existingPid, alreadyRunning: true };
|
|
200
278
|
}
|
|
201
|
-
const appiumBin = getAppiumBin();
|
|
202
279
|
await ensureAppiumHome();
|
|
203
280
|
const logStream = fs.openSync(LOG_FILE, "a");
|
|
204
281
|
const logOffset = fs.fstatSync(logStream).size;
|
|
205
|
-
const
|
|
282
|
+
const [command, args] = getAppiumCommand([
|
|
206
283
|
"--port",
|
|
207
284
|
String(port),
|
|
208
285
|
"--base-path",
|
|
@@ -211,7 +288,8 @@ async function startAppium(port = DEFAULT_PORT, logLevel = "debug") {
|
|
|
211
288
|
"chromedriver_autodownload",
|
|
212
289
|
"--log-level",
|
|
213
290
|
logLevel,
|
|
214
|
-
]
|
|
291
|
+
]);
|
|
292
|
+
const appiumProcess = childProcess.spawn(command, args, {
|
|
215
293
|
detached: true,
|
|
216
294
|
stdio: ["ignore", logStream, logStream],
|
|
217
295
|
env: getAppiumEnv(),
|