@wdio/appium-service 9.0.0-alpha.9 → 9.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.js +235 -5
- package/build/launcher.d.ts +2 -1
- package/build/launcher.d.ts.map +1 -1
- package/build/types.d.ts +5 -0
- package/build/types.d.ts.map +1 -1
- package/package.json +12 -9
- package/build/launcher.js +0 -217
- package/build/types.js +0 -1
- package/build/utils.js +0 -42
- /package/{LICENSE-MIT → LICENSE} +0 -0
package/build/index.js
CHANGED
|
@@ -1,6 +1,236 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
|
|
1
|
+
// src/launcher.ts
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import fsp from "node:fs/promises";
|
|
4
|
+
import url from "node:url";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import treeKill from "tree-kill";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import logger from "@wdio/logger";
|
|
9
|
+
import getPort from "get-port";
|
|
10
|
+
import { resolve as resolve2 } from "import-meta-resolve";
|
|
11
|
+
import { isCloudCapability } from "@wdio/config";
|
|
12
|
+
import { SevereServiceError } from "webdriverio";
|
|
13
|
+
import { isAppiumCapability } from "@wdio/utils";
|
|
14
|
+
|
|
15
|
+
// src/utils.ts
|
|
16
|
+
import { basename, join, resolve } from "node:path";
|
|
17
|
+
import { kebabCase } from "change-case";
|
|
18
|
+
var FILE_EXTENSION_REGEX = /\.[0-9a-z]+$/i;
|
|
19
|
+
function getFilePath(filePath, defaultFilename) {
|
|
20
|
+
let absolutePath = resolve(filePath);
|
|
21
|
+
if (!FILE_EXTENSION_REGEX.test(basename(absolutePath))) {
|
|
22
|
+
absolutePath = join(absolutePath, defaultFilename);
|
|
23
|
+
}
|
|
24
|
+
return absolutePath;
|
|
4
25
|
}
|
|
5
|
-
|
|
6
|
-
|
|
26
|
+
function formatCliArgs(args) {
|
|
27
|
+
const cliArgs = [];
|
|
28
|
+
for (const key in args) {
|
|
29
|
+
const value = args[key];
|
|
30
|
+
if (typeof value === "boolean" && !value || value === null) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
cliArgs.push(`--${kebabCase(key)}`);
|
|
34
|
+
if (typeof value !== "boolean") {
|
|
35
|
+
cliArgs.push(sanitizeCliOptionValue(value));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return cliArgs;
|
|
39
|
+
}
|
|
40
|
+
function sanitizeCliOptionValue(value) {
|
|
41
|
+
const valueString = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
42
|
+
return /\s/.test(valueString) ? `'${valueString}'` : valueString;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/launcher.ts
|
|
46
|
+
var log = logger("@wdio/appium-service");
|
|
47
|
+
var DEFAULT_APPIUM_PORT = 4723;
|
|
48
|
+
var DEFAULT_LOG_FILENAME = "wdio-appium.log";
|
|
49
|
+
var DEFAULT_CONNECTION = {
|
|
50
|
+
protocol: "http",
|
|
51
|
+
hostname: "127.0.0.1",
|
|
52
|
+
path: "/"
|
|
53
|
+
};
|
|
54
|
+
var APPIUM_START_TIMEOUT = 30 * 1e3;
|
|
55
|
+
var AppiumLauncher = class _AppiumLauncher {
|
|
56
|
+
constructor(_options, _capabilities, _config) {
|
|
57
|
+
this._options = _options;
|
|
58
|
+
this._capabilities = _capabilities;
|
|
59
|
+
this._config = _config;
|
|
60
|
+
this._args = {
|
|
61
|
+
basePath: DEFAULT_CONNECTION.path,
|
|
62
|
+
...this._options.args || {}
|
|
63
|
+
};
|
|
64
|
+
this._logPath = _options.logPath || this._config?.outputDir;
|
|
65
|
+
}
|
|
66
|
+
_logPath;
|
|
67
|
+
_appiumCliArgs = [];
|
|
68
|
+
_args;
|
|
69
|
+
_process;
|
|
70
|
+
_isShuttingDown = false;
|
|
71
|
+
async _getCommand(command) {
|
|
72
|
+
if (!command) {
|
|
73
|
+
command = "node";
|
|
74
|
+
this._appiumCliArgs.unshift(await _AppiumLauncher._getAppiumCommand());
|
|
75
|
+
}
|
|
76
|
+
if (process.platform === "win32") {
|
|
77
|
+
this._appiumCliArgs.unshift("/c", command);
|
|
78
|
+
command = "cmd";
|
|
79
|
+
}
|
|
80
|
+
return command;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* update capability connection options to connect
|
|
84
|
+
* to Appium server
|
|
85
|
+
*/
|
|
86
|
+
_setCapabilities(port) {
|
|
87
|
+
if (!Array.isArray(this._capabilities)) {
|
|
88
|
+
for (const [, capability] of Object.entries(this._capabilities)) {
|
|
89
|
+
const cap = capability.capabilities || capability;
|
|
90
|
+
const c = cap.alwaysMatch || cap;
|
|
91
|
+
if (!isCloudCapability(c) && isAppiumCapability(c)) {
|
|
92
|
+
Object.assign(
|
|
93
|
+
capability,
|
|
94
|
+
DEFAULT_CONNECTION,
|
|
95
|
+
{ path: this._args.basePath, port },
|
|
96
|
+
{ ...capability }
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
this._capabilities.forEach((cap) => {
|
|
103
|
+
const w3cCap = cap;
|
|
104
|
+
if (Object.values(cap).length > 0 && Object.values(cap).every((c) => typeof c === "object" && c.capabilities)) {
|
|
105
|
+
Object.values(cap).forEach(
|
|
106
|
+
(c) => {
|
|
107
|
+
const capability = c.capabilities.alwaysMatch || c.capabilities || c;
|
|
108
|
+
if (!isCloudCapability(capability) && isAppiumCapability(capability)) {
|
|
109
|
+
Object.assign(
|
|
110
|
+
c,
|
|
111
|
+
DEFAULT_CONNECTION,
|
|
112
|
+
{ path: this._args.basePath, port },
|
|
113
|
+
{ ...c }
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
} else if (!isCloudCapability(w3cCap.alwaysMatch || cap) && isAppiumCapability(w3cCap.alwaysMatch || cap)) {
|
|
119
|
+
Object.assign(
|
|
120
|
+
cap,
|
|
121
|
+
DEFAULT_CONNECTION,
|
|
122
|
+
{ path: this._args.basePath, port },
|
|
123
|
+
{ ...cap }
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
async onPrepare() {
|
|
129
|
+
if (Array.isArray(this._options.args)) {
|
|
130
|
+
throw new Error("Args should be an object");
|
|
131
|
+
}
|
|
132
|
+
this._args.port = typeof this._args.port === "number" ? this._args.port : await getPort({ port: DEFAULT_APPIUM_PORT });
|
|
133
|
+
this._setCapabilities(this._args.port);
|
|
134
|
+
this._appiumCliArgs.push(...formatCliArgs({ ...this._args }));
|
|
135
|
+
const command = await this._getCommand(this._options.command);
|
|
136
|
+
this._process = await this._startAppium(command, this._appiumCliArgs);
|
|
137
|
+
if (this._logPath) {
|
|
138
|
+
this._redirectLogStream(this._logPath);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
onComplete() {
|
|
142
|
+
this._isShuttingDown = true;
|
|
143
|
+
if (this._process && this._process.pid) {
|
|
144
|
+
log.info("Killing entire Appium tree");
|
|
145
|
+
treeKill(this._process.pid, "SIGTERM", (err) => {
|
|
146
|
+
if (err) {
|
|
147
|
+
log.warn("Failed to kill process:", err);
|
|
148
|
+
} else {
|
|
149
|
+
log.info(
|
|
150
|
+
"Process and its children successfully terminated"
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
_startAppium(command, args, timeout = APPIUM_START_TIMEOUT) {
|
|
157
|
+
log.info(`Will spawn Appium process: ${command} ${args.join(" ")}`);
|
|
158
|
+
const process2 = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
159
|
+
let errorCaptured = false;
|
|
160
|
+
let timeoutId;
|
|
161
|
+
let error;
|
|
162
|
+
return new Promise((resolve3, reject) => {
|
|
163
|
+
let outputBuffer = "";
|
|
164
|
+
timeoutId = setTimeout(() => {
|
|
165
|
+
rejectOnce(new Error("Timeout: Appium did not start within expected time"));
|
|
166
|
+
}, timeout);
|
|
167
|
+
const rejectOnce = (err) => {
|
|
168
|
+
if (!errorCaptured) {
|
|
169
|
+
errorCaptured = true;
|
|
170
|
+
clearTimeout(timeoutId);
|
|
171
|
+
reject(err);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
process2.stdout.on("data", (data) => {
|
|
175
|
+
outputBuffer += data.toString();
|
|
176
|
+
if (outputBuffer.includes("Appium REST http interface listener started")) {
|
|
177
|
+
outputBuffer = "";
|
|
178
|
+
log.info(`Appium started with ID: ${process2.pid}`);
|
|
179
|
+
clearTimeout(timeoutId);
|
|
180
|
+
resolve3(process2);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
process2.stderr.once("data", (data) => {
|
|
184
|
+
error = data.toString() || "Appium exited without unknown error message";
|
|
185
|
+
log.error(error);
|
|
186
|
+
rejectOnce(new Error(error));
|
|
187
|
+
});
|
|
188
|
+
process2.once("exit", (exitCode) => {
|
|
189
|
+
if (this._isShuttingDown) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
let errorMessage = `Appium exited before timeout (exit code: ${exitCode})`;
|
|
193
|
+
if (exitCode === 2) {
|
|
194
|
+
errorMessage += "\n" + (error?.toString() || "Check that you don't already have a running Appium service.");
|
|
195
|
+
} else if (errorCaptured) {
|
|
196
|
+
errorMessage += `
|
|
197
|
+
${error?.toString()}`;
|
|
198
|
+
}
|
|
199
|
+
if (exitCode !== 0) {
|
|
200
|
+
log.error(errorMessage);
|
|
201
|
+
}
|
|
202
|
+
rejectOnce(new Error(errorMessage));
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
async _redirectLogStream(logPath) {
|
|
207
|
+
if (!this._process) {
|
|
208
|
+
throw Error("No Appium process to redirect log stream");
|
|
209
|
+
}
|
|
210
|
+
const logFile = getFilePath(logPath, DEFAULT_LOG_FILENAME);
|
|
211
|
+
await fsp.mkdir(path.dirname(logFile), { recursive: true });
|
|
212
|
+
log.debug(`Appium logs written to: ${logFile}`);
|
|
213
|
+
const logStream = fs.createWriteStream(logFile, { flags: "w" });
|
|
214
|
+
this._process.stdout.pipe(logStream);
|
|
215
|
+
this._process.stderr.pipe(logStream);
|
|
216
|
+
}
|
|
217
|
+
static async _getAppiumCommand(command = "appium") {
|
|
218
|
+
try {
|
|
219
|
+
const entryPath = await resolve2(command, import.meta.url);
|
|
220
|
+
return url.fileURLToPath(entryPath);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
const errorMessage = "Appium is not installed locally. Please install via e.g. `npm i --save-dev appium`.\nIf you use globally installed appium please add: `appium: { command: 'appium' }`\nto your wdio.conf.js!\n\n" + err.stack;
|
|
223
|
+
log.error(errorMessage);
|
|
224
|
+
throw new SevereServiceError(errorMessage);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// src/index.ts
|
|
230
|
+
var AppiumService = class {
|
|
231
|
+
};
|
|
232
|
+
var launcher = AppiumLauncher;
|
|
233
|
+
export {
|
|
234
|
+
AppiumService as default,
|
|
235
|
+
launcher
|
|
236
|
+
};
|
package/build/launcher.d.ts
CHANGED
|
@@ -8,7 +8,8 @@ export default class AppiumLauncher implements Services.ServiceInstance {
|
|
|
8
8
|
private readonly _appiumCliArgs;
|
|
9
9
|
private readonly _args;
|
|
10
10
|
private _process?;
|
|
11
|
-
|
|
11
|
+
private _isShuttingDown;
|
|
12
|
+
constructor(_options: AppiumServiceConfig, _capabilities: Capabilities.TestrunnerCapabilities, _config?: Options.Testrunner | undefined);
|
|
12
13
|
private _getCommand;
|
|
13
14
|
/**
|
|
14
15
|
* update capability connection options to connect
|
package/build/launcher.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"launcher.d.ts","sourceRoot":"","sources":["../src/launcher.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"launcher.d.ts","sourceRoot":"","sources":["../src/launcher.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAIlE,OAAO,KAAK,EAAyB,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAY5E,MAAM,CAAC,OAAO,OAAO,cAAe,YAAW,QAAQ,CAAC,eAAe;IAQ/D,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,OAAO,CAAC;IATpB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAQ;IAClC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAe;IAC9C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAuB;IAC7C,OAAO,CAAC,QAAQ,CAAC,CAA+C;IAChE,OAAO,CAAC,eAAe,CAAiB;gBAG5B,QAAQ,EAAE,mBAAmB,EAC7B,aAAa,EAAE,YAAY,CAAC,sBAAsB,EAClD,OAAO,CAAC,EAAE,OAAO,CAAC,UAAU,YAAA;YAS1B,WAAW;IAqBzB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAkDlB,SAAS;IAiCf,UAAU;IAiBV,OAAO,CAAC,YAAY;YAuEN,kBAAkB;mBAeX,iBAAiB;CAezC"}
|
package/build/types.d.ts
CHANGED
package/build/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,qBAAqB,GAAG;IAChC;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;OAEG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;OAEG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAC9B;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;OAEG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAClC;;OAEG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;OAEG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB;;OAEG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB;;OAEG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;OAEG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAC5B;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;OAEG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,WAAW,yBAAyB;IACtC;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,mBAAmB;IAChC;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,IAAI,CAAC,EAAE,qBAAqB,CAAA;CAC/B;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,CAAA;AAChE,MAAM,MAAM,YAAY,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,CAAA;CAAE,CAAA"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,qBAAqB,GAAG;IAChC;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;OAEG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;OAEG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAC9B;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;OAEG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAClC;;OAEG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;OAEG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB;;OAEG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB;;OAEG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;OAEG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAC5B;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;OAEG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,WAAW,yBAAyB;IACtC;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,mBAAmB;IAChC;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,IAAI,CAAC,EAAE,qBAAqB,CAAA;CAC/B;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,CAAA;AAChE,MAAM,MAAM,YAAY,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,CAAA;CAAE,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wdio/appium-service",
|
|
3
|
-
"version": "9.0.
|
|
3
|
+
"version": "9.0.1",
|
|
4
4
|
"description": "A WebdriverIO service to start & stop Appium Server",
|
|
5
5
|
"author": "Morten Bjerg Gregersen <morten@mogee.dk>",
|
|
6
6
|
"homepage": "https://github.com/webdriverio/webdriverio/tree/main/packages/wdio-appium-service",
|
|
@@ -28,22 +28,25 @@
|
|
|
28
28
|
"type": "module",
|
|
29
29
|
"types": "./build/index.d.ts",
|
|
30
30
|
"exports": {
|
|
31
|
-
".":
|
|
32
|
-
|
|
31
|
+
".": {
|
|
32
|
+
"import": "./build/index.js",
|
|
33
|
+
"types": "./build/index.d.ts"
|
|
34
|
+
}
|
|
33
35
|
},
|
|
34
36
|
"typeScriptVersion": "3.8.3",
|
|
35
37
|
"dependencies": {
|
|
36
|
-
"@wdio/config": "9.0.0
|
|
37
|
-
"@wdio/logger": "9.0.0
|
|
38
|
-
"@wdio/types": "9.0.0
|
|
39
|
-
"@wdio/utils": "9.0.0
|
|
38
|
+
"@wdio/config": "9.0.0",
|
|
39
|
+
"@wdio/logger": "9.0.0",
|
|
40
|
+
"@wdio/types": "9.0.0",
|
|
41
|
+
"@wdio/utils": "9.0.0",
|
|
40
42
|
"change-case": "^5.4.3",
|
|
41
43
|
"get-port": "^7.0.0",
|
|
42
44
|
"import-meta-resolve": "^4.0.0",
|
|
43
|
-
"
|
|
45
|
+
"tree-kill": "^1.2.2",
|
|
46
|
+
"webdriverio": "9.0.1"
|
|
44
47
|
},
|
|
45
48
|
"publishConfig": {
|
|
46
49
|
"access": "public"
|
|
47
50
|
},
|
|
48
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "2a869e5661b2f867d00997c8a17ed0efee0fd15b"
|
|
49
52
|
}
|
package/build/launcher.js
DELETED
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import fsp from 'node:fs/promises';
|
|
3
|
-
import url from 'node:url';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { spawn } from 'node:child_process';
|
|
6
|
-
import logger from '@wdio/logger';
|
|
7
|
-
import getPort from 'get-port';
|
|
8
|
-
import { resolve } from 'import-meta-resolve';
|
|
9
|
-
import { isCloudCapability } from '@wdio/config';
|
|
10
|
-
import { SevereServiceError } from 'webdriverio';
|
|
11
|
-
import { isAppiumCapability } from '@wdio/utils';
|
|
12
|
-
import { getFilePath, formatCliArgs } from './utils.js';
|
|
13
|
-
const log = logger('@wdio/appium-service');
|
|
14
|
-
const DEFAULT_APPIUM_PORT = 4723;
|
|
15
|
-
const DEFAULT_LOG_FILENAME = 'wdio-appium.log';
|
|
16
|
-
const DEFAULT_CONNECTION = {
|
|
17
|
-
protocol: 'http',
|
|
18
|
-
hostname: '127.0.0.1',
|
|
19
|
-
path: '/'
|
|
20
|
-
};
|
|
21
|
-
const APPIUM_START_TIMEOUT = 30 * 1000;
|
|
22
|
-
export default class AppiumLauncher {
|
|
23
|
-
_options;
|
|
24
|
-
_capabilities;
|
|
25
|
-
_config;
|
|
26
|
-
_logPath;
|
|
27
|
-
_appiumCliArgs = [];
|
|
28
|
-
_args;
|
|
29
|
-
_process;
|
|
30
|
-
constructor(_options, _capabilities, _config) {
|
|
31
|
-
this._options = _options;
|
|
32
|
-
this._capabilities = _capabilities;
|
|
33
|
-
this._config = _config;
|
|
34
|
-
this._args = {
|
|
35
|
-
basePath: DEFAULT_CONNECTION.path,
|
|
36
|
-
...(this._options.args || {})
|
|
37
|
-
};
|
|
38
|
-
this._logPath = _options.logPath || this._config?.outputDir;
|
|
39
|
-
}
|
|
40
|
-
async _getCommand(command) {
|
|
41
|
-
/**
|
|
42
|
-
* Explicitly set node as command and appium
|
|
43
|
-
* module path as it's first argument if it's not defined
|
|
44
|
-
*/
|
|
45
|
-
if (!command) {
|
|
46
|
-
command = 'node';
|
|
47
|
-
this._appiumCliArgs.unshift(await AppiumLauncher._getAppiumCommand());
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Windows needs to be started through `cmd` and the command needs to be an arg
|
|
51
|
-
*/
|
|
52
|
-
if (process.platform === 'win32') {
|
|
53
|
-
this._appiumCliArgs.unshift('/c', command);
|
|
54
|
-
command = 'cmd';
|
|
55
|
-
}
|
|
56
|
-
return command;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* update capability connection options to connect
|
|
60
|
-
* to Appium server
|
|
61
|
-
*/
|
|
62
|
-
_setCapabilities(port) {
|
|
63
|
-
/**
|
|
64
|
-
* Multiremote sessions
|
|
65
|
-
*/
|
|
66
|
-
if (!Array.isArray(this._capabilities)) {
|
|
67
|
-
for (const [, capability] of Object.entries(this._capabilities)) {
|
|
68
|
-
const cap = capability.capabilities || capability;
|
|
69
|
-
const c = cap.alwaysMatch || cap;
|
|
70
|
-
if (!isCloudCapability(c) && isAppiumCapability(c)) {
|
|
71
|
-
Object.assign(capability, DEFAULT_CONNECTION, { path: this._args.basePath, port }, { ...capability });
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
this._capabilities.forEach((cap) => {
|
|
77
|
-
const w3cCap = cap;
|
|
78
|
-
/**
|
|
79
|
-
* Parallel Multiremote
|
|
80
|
-
*/
|
|
81
|
-
if (Object.values(cap).length > 0 && Object.values(cap).every(c => typeof c === 'object' && c.capabilities)) {
|
|
82
|
-
Object.values(cap).forEach(c => {
|
|
83
|
-
const capability = c.capabilities.alwaysMatch || c.capabilities || c;
|
|
84
|
-
if (!isCloudCapability(capability) && isAppiumCapability(capability)) {
|
|
85
|
-
Object.assign(c, DEFAULT_CONNECTION, { path: this._args.basePath, port }, { ...c });
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
else if (!isCloudCapability(w3cCap.alwaysMatch || cap) && isAppiumCapability(w3cCap.alwaysMatch || cap)) {
|
|
90
|
-
Object.assign(cap, DEFAULT_CONNECTION, { path: this._args.basePath, port }, { ...cap });
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
async onPrepare() {
|
|
95
|
-
/**
|
|
96
|
-
* Throws an error if `this._options.args` is defined and is an array.
|
|
97
|
-
* @throws {Error} If `this._options.args` is an array.
|
|
98
|
-
*/
|
|
99
|
-
if (Array.isArray(this._options.args)) {
|
|
100
|
-
throw new Error('Args should be an object');
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* Append remaining arguments
|
|
104
|
-
*/
|
|
105
|
-
this._appiumCliArgs.push(...formatCliArgs(this._args));
|
|
106
|
-
/**
|
|
107
|
-
* Get port from service option or use a random port
|
|
108
|
-
*/
|
|
109
|
-
const port = typeof this._args.port === 'number'
|
|
110
|
-
? this._args.port
|
|
111
|
-
: await getPort({ port: DEFAULT_APPIUM_PORT });
|
|
112
|
-
this._setCapabilities(port);
|
|
113
|
-
/**
|
|
114
|
-
* start Appium
|
|
115
|
-
*/
|
|
116
|
-
const command = await this._getCommand(this._options.command);
|
|
117
|
-
this._process = await this._startAppium(command, this._appiumCliArgs);
|
|
118
|
-
if (this._logPath) {
|
|
119
|
-
this._redirectLogStream(this._logPath);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
onComplete() {
|
|
123
|
-
if (this._process) {
|
|
124
|
-
log.info(`Appium (pid: ${this._process.pid}) killed`);
|
|
125
|
-
this._process.kill();
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
_startAppium(command, args, timeout = APPIUM_START_TIMEOUT) {
|
|
129
|
-
log.info(`Will spawn Appium process: ${command} ${args.join(' ')}`);
|
|
130
|
-
const process = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
131
|
-
// just for validate the first error
|
|
132
|
-
let errorCaptured = false;
|
|
133
|
-
// to set a timeout for the promise
|
|
134
|
-
let timeoutId;
|
|
135
|
-
// to store the first error message
|
|
136
|
-
let error;
|
|
137
|
-
return new Promise((resolve, reject) => {
|
|
138
|
-
let outputBuffer = '';
|
|
139
|
-
/**
|
|
140
|
-
* set timeout for promise. If Appium does not start within given timeout,
|
|
141
|
-
* e.g. if the port is already in use, reject the promise.
|
|
142
|
-
*/
|
|
143
|
-
timeoutId = setTimeout(() => {
|
|
144
|
-
rejectOnce(new Error('Timeout: Appium did not start within expected time'));
|
|
145
|
-
}, timeout);
|
|
146
|
-
/**
|
|
147
|
-
* reject promise if Appium does not start within given timeout,
|
|
148
|
-
* e.g. if the port is already in use
|
|
149
|
-
*
|
|
150
|
-
* @param err - error to reject with
|
|
151
|
-
*/
|
|
152
|
-
const rejectOnce = (err) => {
|
|
153
|
-
if (!errorCaptured) {
|
|
154
|
-
errorCaptured = true;
|
|
155
|
-
clearTimeout(timeoutId);
|
|
156
|
-
reject(err);
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
process.stdout.on('data', (data) => {
|
|
160
|
-
outputBuffer += data.toString();
|
|
161
|
-
if (outputBuffer.includes('Appium REST http interface listener started')) {
|
|
162
|
-
outputBuffer = '';
|
|
163
|
-
log.info(`Appium started with ID: ${process.pid}`);
|
|
164
|
-
clearTimeout(timeoutId);
|
|
165
|
-
resolve(process);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
/**
|
|
169
|
-
* only capture first error to print it in case Appium failed to start.
|
|
170
|
-
*/
|
|
171
|
-
process.stderr.once('data', (data) => {
|
|
172
|
-
error = data.toString() || 'Appium exited without unknown error message';
|
|
173
|
-
log.error(error);
|
|
174
|
-
rejectOnce(new Error(error));
|
|
175
|
-
});
|
|
176
|
-
process.once('exit', (exitCode) => {
|
|
177
|
-
let errorMessage = `Appium exited before timeout (exit code: ${exitCode})`;
|
|
178
|
-
if (exitCode === 2) {
|
|
179
|
-
errorMessage += '\n' + (error?.toString() || 'Check that you don\'t already have a running Appium service.');
|
|
180
|
-
}
|
|
181
|
-
else if (errorCaptured) {
|
|
182
|
-
errorMessage += `\n${error?.toString()}`;
|
|
183
|
-
}
|
|
184
|
-
if (exitCode !== 0) {
|
|
185
|
-
log.error(errorMessage);
|
|
186
|
-
}
|
|
187
|
-
rejectOnce(new Error(errorMessage));
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
async _redirectLogStream(logPath) {
|
|
192
|
-
if (!this._process) {
|
|
193
|
-
throw Error('No Appium process to redirect log stream');
|
|
194
|
-
}
|
|
195
|
-
const logFile = getFilePath(logPath, DEFAULT_LOG_FILENAME);
|
|
196
|
-
// ensure file & directory exists
|
|
197
|
-
await fsp.mkdir(path.dirname(logFile), { recursive: true });
|
|
198
|
-
log.debug(`Appium logs written to: ${logFile}`);
|
|
199
|
-
const logStream = fs.createWriteStream(logFile, { flags: 'w' });
|
|
200
|
-
this._process.stdout.pipe(logStream);
|
|
201
|
-
this._process.stderr.pipe(logStream);
|
|
202
|
-
}
|
|
203
|
-
static async _getAppiumCommand(command = 'appium') {
|
|
204
|
-
try {
|
|
205
|
-
const entryPath = await resolve(command, import.meta.url);
|
|
206
|
-
return url.fileURLToPath(entryPath);
|
|
207
|
-
}
|
|
208
|
-
catch (err) {
|
|
209
|
-
const errorMessage = ('Appium is not installed locally. Please install via e.g. `npm i --save-dev appium`.\n' +
|
|
210
|
-
'If you use globally installed appium please add: `appium: { command: \'appium\' }`\n' +
|
|
211
|
-
'to your wdio.conf.js!\n\n' +
|
|
212
|
-
err.stack);
|
|
213
|
-
log.error(errorMessage);
|
|
214
|
-
throw new SevereServiceError(errorMessage);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
package/build/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/build/utils.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { basename, join, resolve } from 'node:path';
|
|
2
|
-
import { kebabCase } from 'change-case';
|
|
3
|
-
const FILE_EXTENSION_REGEX = /\.[0-9a-z]+$/i;
|
|
4
|
-
/**
|
|
5
|
-
* Resolves the given path into a absolute path and appends the default filename as fallback when the provided path is a directory.
|
|
6
|
-
* @param {string} filePath relative file or directory path
|
|
7
|
-
* @param {string} defaultFilename default file name when filePath is a directory
|
|
8
|
-
* @return {String} absolute file path
|
|
9
|
-
*/
|
|
10
|
-
export function getFilePath(filePath, defaultFilename) {
|
|
11
|
-
let absolutePath = resolve(filePath);
|
|
12
|
-
// test if we already have a file (e.g. selenium.txt, .log, log.txt, etc.)
|
|
13
|
-
// NOTE: path.extname doesn't work to detect a file, cause dotfiles are reported by node to have no extension
|
|
14
|
-
if (!FILE_EXTENSION_REGEX.test(basename(absolutePath))) {
|
|
15
|
-
absolutePath = join(absolutePath, defaultFilename);
|
|
16
|
-
}
|
|
17
|
-
return absolutePath;
|
|
18
|
-
}
|
|
19
|
-
export function formatCliArgs(args) {
|
|
20
|
-
const cliArgs = [];
|
|
21
|
-
for (const key in args) {
|
|
22
|
-
const value = args[key];
|
|
23
|
-
// If the value is false or null the argument is discarded
|
|
24
|
-
if ((typeof value === 'boolean' && !value) || value === null) {
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
cliArgs.push(`--${kebabCase(key)}`);
|
|
28
|
-
// Only non-boolean and non-null values are added as option values
|
|
29
|
-
if (typeof value !== 'boolean') {
|
|
30
|
-
cliArgs.push(sanitizeCliOptionValue(value));
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return cliArgs;
|
|
34
|
-
}
|
|
35
|
-
export function sanitizeCliOptionValue(value) {
|
|
36
|
-
const valueString = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
37
|
-
// Encapsulate the value string in single quotes if it contains a white space
|
|
38
|
-
return /\s/.test(valueString) ? `'${valueString}'` : valueString;
|
|
39
|
-
}
|
|
40
|
-
export function isWindows() {
|
|
41
|
-
return process.platform === 'win32';
|
|
42
|
-
}
|
/package/{LICENSE-MIT → LICENSE}
RENAMED
|
File without changes
|