appium-webdriveragent 14.2.0 → 15.0.0

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/WebDriverAgentLib/Info.plist +2 -2
  3. package/build/lib/types.d.ts +44 -4
  4. package/build/lib/types.d.ts.map +1 -1
  5. package/build/lib/utils/index.d.ts +11 -0
  6. package/build/lib/utils/index.d.ts.map +1 -0
  7. package/build/lib/utils/index.js +27 -0
  8. package/build/lib/utils/index.js.map +1 -0
  9. package/build/lib/utils/module.d.ts +6 -0
  10. package/build/lib/utils/module.d.ts.map +1 -0
  11. package/build/lib/utils/module.js +31 -0
  12. package/build/lib/utils/module.js.map +1 -0
  13. package/build/lib/utils/platform.d.ts +7 -0
  14. package/build/lib/utils/platform.d.ts.map +1 -0
  15. package/build/lib/utils/platform.js +13 -0
  16. package/build/lib/utils/platform.js.map +1 -0
  17. package/build/lib/utils/processes.d.ts +23 -0
  18. package/build/lib/utils/processes.d.ts.map +1 -0
  19. package/build/lib/utils/processes.js +132 -0
  20. package/build/lib/utils/processes.js.map +1 -0
  21. package/build/lib/utils/security.d.ts +5 -0
  22. package/build/lib/utils/security.d.ts.map +1 -0
  23. package/build/lib/utils/security.js +15 -0
  24. package/build/lib/utils/security.js.map +1 -0
  25. package/build/lib/utils/xctestrun.d.ts +51 -0
  26. package/build/lib/utils/xctestrun.d.ts.map +1 -0
  27. package/build/lib/utils/xctestrun.js +120 -0
  28. package/build/lib/utils/xctestrun.js.map +1 -0
  29. package/build/lib/wda-strategies.d.ts +69 -0
  30. package/build/lib/wda-strategies.d.ts.map +1 -0
  31. package/build/lib/wda-strategies.js +253 -0
  32. package/build/lib/wda-strategies.js.map +1 -0
  33. package/build/lib/webdriveragent.d.ts +3 -14
  34. package/build/lib/webdriveragent.d.ts.map +1 -1
  35. package/build/lib/webdriveragent.js +78 -127
  36. package/build/lib/webdriveragent.js.map +1 -1
  37. package/build/lib/xcodebuild.d.ts.map +1 -1
  38. package/build/lib/xcodebuild.js +29 -7
  39. package/build/lib/xcodebuild.js.map +1 -1
  40. package/lib/types.ts +58 -4
  41. package/lib/utils/index.ts +20 -0
  42. package/lib/utils/module.ts +29 -0
  43. package/lib/utils/platform.ts +10 -0
  44. package/lib/utils/processes.ts +135 -0
  45. package/lib/utils/security.ts +15 -0
  46. package/lib/utils/xctestrun.ts +160 -0
  47. package/lib/wda-strategies.ts +340 -0
  48. package/lib/webdriveragent.ts +89 -163
  49. package/lib/xcodebuild.ts +34 -17
  50. package/package.json +3 -4
  51. package/build/lib/utils.d.ts +0 -105
  52. package/build/lib/utils.d.ts.map +0 -1
  53. package/build/lib/utils.js +0 -413
  54. package/build/lib/utils.js.map +0 -1
  55. package/lib/utils.ts +0 -442
package/lib/utils.ts DELETED
@@ -1,442 +0,0 @@
1
- import {fs, plist} from '@appium/support';
2
- import {exec} from 'teen_process';
3
- import type {SubProcess} from 'teen_process';
4
- import path, {dirname} from 'node:path';
5
- import {fileURLToPath} from 'node:url';
6
- import {log} from './logger';
7
- import {PLATFORM_NAME_TVOS} from './constants';
8
- import _fs from 'node:fs';
9
- import {waitForCondition} from 'asyncbox';
10
- import {arch} from 'node:os';
11
- import type {DeviceInfo} from './types';
12
-
13
- // Get current filename - works in both CommonJS and ESM
14
- const currentFilename =
15
- typeof __filename !== 'undefined'
16
- ? __filename
17
- : fileURLToPath(new Function('return import.meta.url')());
18
-
19
- const currentDirname = dirname(currentFilename);
20
-
21
- let moduleRootCache: string | undefined;
22
-
23
- /**
24
- * Calculates the path to the current module's root folder
25
- *
26
- * @returns {string} The full path to module root
27
- * @throws {Error} If the current module root folder cannot be determined
28
- */
29
- const getModuleRoot = function getModuleRoot(): string {
30
- if (moduleRootCache) {
31
- return moduleRootCache;
32
- }
33
- let currentDir = currentDirname;
34
- let isAtFsRoot = false;
35
- while (!isAtFsRoot) {
36
- const manifestPath = path.join(currentDir, 'package.json');
37
- try {
38
- if (
39
- _fs.existsSync(manifestPath) &&
40
- JSON.parse(_fs.readFileSync(manifestPath, 'utf8')).name === 'appium-webdriveragent'
41
- ) {
42
- moduleRootCache = currentDir;
43
- return currentDir;
44
- }
45
- } catch {}
46
- currentDir = path.dirname(currentDir);
47
- isAtFsRoot = currentDir.length <= path.dirname(currentDir).length;
48
- }
49
- throw new Error('Cannot find the root folder of the appium-webdriveragent Node.js module');
50
- };
51
-
52
- export const BOOTSTRAP_PATH = getModuleRoot();
53
-
54
- /**
55
- * Arguments for setting xctestrun file
56
- */
57
- export interface XctestrunFileArgs {
58
- deviceInfo: DeviceInfo;
59
- sdkVersion: string;
60
- bootstrapPath: string;
61
- wdaRemotePort: number | string;
62
- wdaBindingIP?: string;
63
- maxHttpRequestBodySize?: number | string;
64
- }
65
-
66
- /**
67
- * Find and terminate all processes matching the given pgrep pattern.
68
- */
69
- export async function killAppUsingPattern(pgrepPattern: string): Promise<void> {
70
- const signals = [2, 15, 9];
71
- for (const signal of signals) {
72
- const matchedPids = await getPIDsUsingPattern(pgrepPattern);
73
- if (matchedPids.length === 0) {
74
- return;
75
- }
76
- const args = [`-${signal}`, ...matchedPids];
77
- try {
78
- await exec('kill', args);
79
- } catch (err: any) {
80
- log.debug(`kill ${args.join(' ')} -> ${err.message}`);
81
- }
82
- if (signal === signals[signals.length - 1]) {
83
- // there is no need to wait after SIGKILL
84
- return;
85
- }
86
- try {
87
- await waitForCondition(
88
- async () => {
89
- const pidCheckPromises = matchedPids.map(async (pid) => {
90
- try {
91
- await exec('kill', ['-0', pid]);
92
- // the process is still alive
93
- return false;
94
- } catch {
95
- // the process is dead
96
- return true;
97
- }
98
- });
99
- return (await Promise.all(pidCheckPromises)).every((x) => x === true);
100
- },
101
- {
102
- waitMs: 1000,
103
- intervalMs: 100,
104
- },
105
- );
106
- return;
107
- } catch {
108
- // try the next signal
109
- }
110
- }
111
- }
112
-
113
- /**
114
- * Return true if the platformName is tvOS
115
- * @param platformName The name of the platorm
116
- * @returns Return true if the platformName is tvOS
117
- */
118
- export function isTvOS(platformName: string): boolean {
119
- return platformName?.toLowerCase() === PLATFORM_NAME_TVOS.toLowerCase();
120
- }
121
-
122
- /**
123
- * Configure keychain access required for real-device code signing.
124
- */
125
- export async function setRealDeviceSecurity(
126
- keychainPath: string,
127
- keychainPassword: string,
128
- ): Promise<void> {
129
- log.debug('Setting security for iOS device');
130
- await exec('security', ['-v', 'list-keychains', '-s', keychainPath]);
131
- await exec('security', ['-v', 'unlock-keychain', '-p', keychainPassword, keychainPath]);
132
- await exec('security', ['set-keychain-settings', '-t', '3600', '-l', keychainPath]);
133
- }
134
-
135
- /**
136
- * Creates xctestrun file per device & platform version.
137
- * We expects to have WebDriverAgentRunner_iphoneos${sdkVersion|platformVersion}-arm64.xctestrun for real device
138
- * and WebDriverAgentRunner_iphonesimulator${sdkVersion|platformVersion}-${x86_64|arm64}.xctestrun for simulator located @bootstrapPath
139
- * Newer Xcode (Xcode 10.0 at least) generate xctestrun file following sdkVersion.
140
- * e.g. Xcode which has iOS SDK Version 12.2 on an intel Mac host machine generates WebDriverAgentRunner_iphonesimulator.2-x86_64.xctestrun
141
- * even if the cap has platform version 11.4
142
- *
143
- * @param args
144
- * @return returns xctestrunFilePath for given device
145
- * @throws if WebDriverAgentRunner_iphoneos${sdkVersion|platformVersion}-arm64.xctestrun for real device
146
- * or WebDriverAgentRunner_iphonesimulator${sdkVersion|platformVersion}-x86_64.xctestrun for simulator is not found @bootstrapPath,
147
- * then it will throw a file not found exception
148
- */
149
- export async function setXctestrunFile(args: XctestrunFileArgs): Promise<string> {
150
- const {
151
- deviceInfo,
152
- sdkVersion,
153
- bootstrapPath,
154
- wdaRemotePort,
155
- wdaBindingIP,
156
- maxHttpRequestBodySize,
157
- } = args;
158
- const xctestrunFilePath = await getXctestrunFilePath(deviceInfo, sdkVersion, bootstrapPath);
159
- const xctestRunContent = await plist.parsePlistFile(xctestrunFilePath);
160
- const updateWDAPort = getAdditionalRunContent(
161
- deviceInfo.platformName,
162
- wdaRemotePort,
163
- wdaBindingIP,
164
- maxHttpRequestBodySize,
165
- );
166
- const newXctestRunContent = mergeObjects(xctestRunContent, updateWDAPort);
167
- await plist.updatePlistFile(xctestrunFilePath, newXctestRunContent, true);
168
-
169
- return xctestrunFilePath;
170
- }
171
-
172
- /**
173
- * Return the WDA object which appends existing xctest runner content
174
- * @param platformName - The name of the platform
175
- * @param wdaRemotePort - The remote port number
176
- * @param wdaBindingIP - The IP address to bind to. If not given, it binds to all interfaces.
177
- * @param maxHttpRequestBodySize - The maximum HTTP request body size in bytes.
178
- * @return returns a runner object which has USE_PORT and optionally USE_IP
179
- */
180
- export function getAdditionalRunContent(
181
- platformName: string,
182
- wdaRemotePort: number | string,
183
- wdaBindingIP?: string,
184
- maxHttpRequestBodySize?: number | string,
185
- ): Record<string, any> {
186
- const runner = `WebDriverAgentRunner${isTvOS(platformName) ? '_tvOS' : ''}`;
187
- return {
188
- [runner]: {
189
- EnvironmentVariables: {
190
- // USE_PORT must be 'string'
191
- USE_PORT: `${wdaRemotePort}`,
192
- ...(wdaBindingIP ? {USE_IP: wdaBindingIP} : {}),
193
- ...(maxHttpRequestBodySize
194
- ? {MAX_HTTP_REQUEST_BODY_SIZE: `${maxHttpRequestBodySize}`}
195
- : {}),
196
- },
197
- },
198
- };
199
- }
200
-
201
- /**
202
- * Return the path of xctestrun if it exists
203
- * @param deviceInfo
204
- * @param sdkVersion - The Xcode SDK version of OS.
205
- * @param bootstrapPath - The folder path containing xctestrun file.
206
- */
207
- export async function getXctestrunFilePath(
208
- deviceInfo: DeviceInfo,
209
- sdkVersion: string,
210
- bootstrapPath: string,
211
- ): Promise<string> {
212
- // First try the SDK path, for Xcode 10 (at least)
213
- const sdkBased: [string, string] = [
214
- path.resolve(bootstrapPath, `${deviceInfo.udid}_${sdkVersion}.xctestrun`),
215
- sdkVersion,
216
- ];
217
- // Next try Platform path, for earlier Xcode versions
218
- const platformBased: [string, string] = [
219
- path.resolve(bootstrapPath, `${deviceInfo.udid}_${deviceInfo.platformVersion}.xctestrun`),
220
- deviceInfo.platformVersion,
221
- ];
222
-
223
- for (const [filePath, version] of [sdkBased, platformBased]) {
224
- if (await fs.exists(filePath)) {
225
- log.info(`Using '${filePath}' as xctestrun file`);
226
- return filePath;
227
- }
228
- const originalXctestrunFile = path.resolve(
229
- bootstrapPath,
230
- getXctestrunFileName(deviceInfo, version),
231
- );
232
- if (await fs.exists(originalXctestrunFile)) {
233
- // If this is first time run for given device, then first generate xctestrun file for device.
234
- // We need to have a xctestrun file **per device** because we cant not have same wda port for all devices.
235
- await fs.copyFile(originalXctestrunFile, filePath);
236
- log.info(`Using '${filePath}' as xctestrun file copied by '${originalXctestrunFile}'`);
237
- return filePath;
238
- }
239
- }
240
-
241
- throw new Error(
242
- `If you are using 'useXctestrunFile' capability then you ` +
243
- `need to have a xctestrun file (expected: ` +
244
- `'${path.resolve(bootstrapPath, getXctestrunFileName(deviceInfo, sdkVersion))}')`,
245
- );
246
- }
247
-
248
- /**
249
- * Return the name of xctestrun file
250
- * @param deviceInfo
251
- * @param version - The Xcode SDK version of OS.
252
- * @return returns xctestrunFilePath for given device
253
- */
254
- export function getXctestrunFileName(deviceInfo: DeviceInfo, version: string): string {
255
- const archSuffix = deviceInfo.isRealDevice
256
- ? `os${version}-arm64`
257
- : `simulator${version}-${arch() === 'arm64' ? 'arm64' : 'x86_64'}`;
258
- return `WebDriverAgentRunner_${isTvOS(deviceInfo.platformName) ? 'tvOS_appletv' : 'iphone'}${archSuffix}.xctestrun`;
259
- }
260
-
261
- /**
262
- * Ensures the process is killed after the timeout
263
- */
264
- export async function killProcess(
265
- name: string,
266
- proc: SubProcess | null | undefined,
267
- ): Promise<void> {
268
- if (!proc || !proc.isRunning) {
269
- return;
270
- }
271
-
272
- log.info(`Shutting down '${name}' process (pid '${proc.proc?.pid}')`);
273
-
274
- log.info(`Sending 'SIGTERM'...`);
275
- try {
276
- await proc.stop('SIGTERM', 1000);
277
- return;
278
- } catch (err: any) {
279
- if (!err.message.includes(`Process didn't end after`)) {
280
- throw err;
281
- }
282
- log.debug(`${name} process did not end in a timely fashion: '${err.message}'.`);
283
- }
284
-
285
- log.info(`Sending 'SIGKILL'...`);
286
- try {
287
- await proc.stop('SIGKILL');
288
- } catch (err: any) {
289
- if (err.message.includes('not currently running')) {
290
- // the process ended but for some reason we were not informed
291
- return;
292
- }
293
- throw err;
294
- }
295
- }
296
-
297
- /**
298
- * Generate a random integer in range [low, high). `low` is inclusive and `high` is exclusive.
299
- */
300
- export function randomInt(low: number, high: number): number {
301
- return Math.floor(Math.random() * (high - low) + low);
302
- }
303
-
304
- /**
305
- * Retrieves WDA upgrade timestamp. The manifest only gets modified on package upgrade.
306
- */
307
- export async function getWDAUpgradeTimestamp(): Promise<number | null> {
308
- const packageManifest = path.resolve(getModuleRoot(), 'package.json');
309
- if (!(await fs.exists(packageManifest))) {
310
- return null;
311
- }
312
- const {mtime} = await fs.stat(packageManifest);
313
- return mtime.getTime();
314
- }
315
-
316
- /**
317
- * Escape regular expression metacharacters in a string.
318
- */
319
- export function escapeRegExp(value: string): string {
320
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
321
- }
322
-
323
- /**
324
- * Truncate a string to the given length and append ellipsis if needed.
325
- */
326
- export function truncateString(value: string, length: number): string {
327
- if (value.length <= length) {
328
- return value;
329
- }
330
- return `${value.slice(0, Math.max(0, length - 1))}…`;
331
- }
332
-
333
- /**
334
- * Kills running XCTest processes for the particular device.
335
- */
336
- export async function resetTestProcesses(udid: string, isSimulator: boolean): Promise<void> {
337
- const processPatterns = [`xcodebuild.*${udid}`];
338
- if (isSimulator) {
339
- processPatterns.push(`${udid}.*XCTRunner`);
340
- // Some XCTest launches might not include xcodebuild in their command line
341
- processPatterns.push(`xctest.*${udid}`);
342
- }
343
- log.debug(`Killing running processes '${processPatterns.join(', ')}' for the device ${udid}...`);
344
- await Promise.all(processPatterns.map(killAppUsingPattern));
345
- }
346
-
347
- /**
348
- * Get the IDs of processes listening on the particular system port.
349
- * It is also possible to apply additional filtering based on the
350
- * process command line.
351
- *
352
- * @param port - The port number.
353
- * @param filteringFunc - Optional lambda function, which
354
- * receives command line string of the particular process
355
- * listening on given port, and is expected to return
356
- * either true or false to include/exclude the corresponding PID
357
- * from the resulting array.
358
- * @returns - the list of matched process ids.
359
- */
360
- export async function getPIDsListeningOnPort(
361
- port: string | number,
362
- filteringFunc: ((cmdline: string) => boolean | Promise<boolean>) | null = null,
363
- ): Promise<string[]> {
364
- const result: string[] = [];
365
- try {
366
- // This only works since Mac OS X El Capitan
367
- const {stdout} = await exec('lsof', ['-ti', `tcp:${port}`]);
368
- result.push(...stdout.trim().split(/\n+/));
369
- } catch (e: any) {
370
- if (e.code !== 1) {
371
- // code 1 means no processes. Other errors need reporting
372
- log.debug(`Error getting processes listening on port '${port}': ${e.stderr || e.message}`);
373
- }
374
- return result;
375
- }
376
-
377
- if (typeof filteringFunc !== 'function') {
378
- return result;
379
- }
380
- const filtered = await Promise.all(
381
- result.map(async (pid) => {
382
- let stdout: string;
383
- try {
384
- ({stdout} = await exec('ps', ['-p', pid, '-o', 'command']));
385
- } catch (e: any) {
386
- if (e.code === 1) {
387
- // The process does not exist anymore, there's nothing to filter
388
- return null;
389
- }
390
- throw e;
391
- }
392
- return (await filteringFunc(stdout)) ? pid : null;
393
- }),
394
- );
395
- return filtered.filter((pid): pid is string => Boolean(pid));
396
- }
397
-
398
- // Private functions
399
-
400
- async function getPIDsUsingPattern(pattern: string): Promise<string[]> {
401
- const args = [
402
- '-if', // case insensitive, full cmdline match
403
- pattern,
404
- ];
405
- try {
406
- const {stdout} = await exec('pgrep', args);
407
- return stdout
408
- .split(/\s+/)
409
- .map((x) => parseInt(x, 10))
410
- .filter(Number.isInteger)
411
- .map((x) => `${x}`);
412
- } catch (err: any) {
413
- log.debug(
414
- `'pgrep ${args.join(' ')}' didn't detect any matching processes. Return code: ${err.code}`,
415
- );
416
- return [];
417
- }
418
- }
419
-
420
- function mergeObjects<T extends Record<string, any>, U extends Record<string, any>>(
421
- target: T,
422
- source: U,
423
- ): T & U {
424
- const output: Record<string, any> = {...target};
425
- for (const [key, sourceValue] of Object.entries(source)) {
426
- const targetValue = output[key];
427
- if (isPlainObject(targetValue) && isPlainObject(sourceValue)) {
428
- output[key] = mergeObjects(targetValue, sourceValue);
429
- continue;
430
- }
431
- output[key] = sourceValue;
432
- }
433
- return output as T & U;
434
- }
435
-
436
- function isPlainObject(value: unknown): value is Record<string, any> {
437
- if (value == null || typeof value !== 'object' || Array.isArray(value)) {
438
- return false;
439
- }
440
- const prototype = Object.getPrototypeOf(value);
441
- return prototype === Object.prototype || prototype === null;
442
- }