@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 CHANGED
@@ -1,6 +1,236 @@
1
- /* istanbul ignore file */
2
- import AppiumLauncher from './launcher.js';
3
- export default class AppiumService {
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
- export const launcher = AppiumLauncher;
6
- export * from './types.js';
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
+ };
@@ -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
- constructor(_options: AppiumServiceConfig, _capabilities: Capabilities.RemoteCapabilities, _config?: Options.Testrunner | undefined);
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
@@ -1 +1 @@
1
- {"version":3,"file":"launcher.d.ts","sourceRoot":"","sources":["../src/launcher.ts"],"names":[],"mappings":"AAYA,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;IAO/D,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,OAAO,CAAC;IARpB,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;gBAGpD,QAAQ,EAAE,mBAAmB,EAC7B,aAAa,EAAE,YAAY,CAAC,kBAAkB,EAC9C,OAAO,CAAC,gCAAoB;YAS1B,WAAW;IAqBzB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAkDlB,SAAS;IAgCf,UAAU;IAMV,OAAO,CAAC,YAAY;YAoEN,kBAAkB;mBAeX,iBAAiB;CAezC"}
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
@@ -47,6 +47,11 @@ export type AppiumServerArguments = {
47
47
  * Send log output to the file
48
48
  */
49
49
  log?: string;
50
+ /**
51
+ * Log Filters
52
+ * A path to a valid JSON file containing an array of filtering rules
53
+ */
54
+ logFilters?: string;
50
55
  /**
51
56
  * Log level
52
57
  */
@@ -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.0-alpha.9+9220932b7",
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
- ".": "./build/index.js",
32
- "./package.json": "./package.json"
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-alpha.9+9220932b7",
37
- "@wdio/logger": "9.0.0-alpha.9+9220932b7",
38
- "@wdio/types": "9.0.0-alpha.9+9220932b7",
39
- "@wdio/utils": "9.0.0-alpha.9+9220932b7",
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
- "webdriverio": "9.0.0-alpha.9+9220932b7"
45
+ "tree-kill": "^1.2.2",
46
+ "webdriverio": "9.0.1"
44
47
  },
45
48
  "publishConfig": {
46
49
  "access": "public"
47
50
  },
48
- "gitHead": "9220932b7048d9b5b6c8397dda54842625be7ef2"
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
- }
File without changes