appium-ios-simulator 8.1.2 → 8.2.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 (90) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/build/lib/extensions/applications.d.ts.map +1 -1
  3. package/build/lib/extensions/applications.js +2 -3
  4. package/build/lib/extensions/applications.js.map +1 -1
  5. package/build/lib/extensions/biometric.js +2 -2
  6. package/build/lib/extensions/biometric.js.map +1 -1
  7. package/build/lib/extensions/keychain.d.ts.map +1 -1
  8. package/build/lib/extensions/keychain.js +5 -9
  9. package/build/lib/extensions/keychain.js.map +1 -1
  10. package/build/lib/extensions/permissions.js +2 -2
  11. package/build/lib/extensions/permissions.js.map +1 -1
  12. package/build/lib/extensions/settings.d.ts.map +1 -1
  13. package/build/lib/extensions/settings.js +10 -11
  14. package/build/lib/extensions/settings.js.map +1 -1
  15. package/build/lib/simulator-xcode-14.d.ts +2 -0
  16. package/build/lib/simulator-xcode-14.d.ts.map +1 -1
  17. package/build/lib/simulator-xcode-14.js +19 -22
  18. package/build/lib/simulator-xcode-14.js.map +1 -1
  19. package/build/lib/simulator-xcode-15.d.ts.map +1 -1
  20. package/build/lib/simulator-xcode-15.js +2 -16
  21. package/build/lib/simulator-xcode-15.js.map +1 -1
  22. package/build/lib/simulator-xcode-27.d.ts +6 -0
  23. package/build/lib/simulator-xcode-27.d.ts.map +1 -0
  24. package/build/lib/simulator-xcode-27.js +13 -0
  25. package/build/lib/simulator-xcode-27.js.map +1 -0
  26. package/build/lib/simulator.d.ts.map +1 -1
  27. package/build/lib/simulator.js +9 -8
  28. package/build/lib/simulator.js.map +1 -1
  29. package/build/lib/utils/constants.d.ts +7 -0
  30. package/build/lib/utils/constants.d.ts.map +1 -0
  31. package/build/lib/utils/constants.js +10 -0
  32. package/build/lib/utils/constants.js.map +1 -0
  33. package/build/lib/{defaults-utils.d.ts → utils/defaults.d.ts} +1 -1
  34. package/build/lib/utils/defaults.d.ts.map +1 -0
  35. package/build/lib/{defaults-utils.js → utils/defaults.js} +8 -8
  36. package/build/lib/utils/defaults.js.map +1 -0
  37. package/build/lib/utils/devices.d.ts +13 -0
  38. package/build/lib/utils/devices.d.ts.map +1 -0
  39. package/build/lib/utils/devices.js +24 -0
  40. package/build/lib/utils/devices.js.map +1 -0
  41. package/build/lib/utils/get-devices.d.ts +7 -0
  42. package/build/lib/utils/get-devices.d.ts.map +1 -0
  43. package/build/lib/utils/get-devices.js +12 -0
  44. package/build/lib/utils/get-devices.js.map +1 -0
  45. package/build/lib/utils/index.d.ts +9 -0
  46. package/build/lib/utils/index.d.ts.map +1 -0
  47. package/build/lib/utils/index.js +29 -0
  48. package/build/lib/utils/index.js.map +1 -0
  49. package/build/lib/utils/lifecycle.d.ts +6 -0
  50. package/build/lib/utils/lifecycle.d.ts.map +1 -0
  51. package/build/lib/utils/lifecycle.js +66 -0
  52. package/build/lib/utils/lifecycle.js.map +1 -0
  53. package/build/lib/utils/process.d.ts +11 -0
  54. package/build/lib/utils/process.d.ts.map +1 -0
  55. package/build/lib/utils/process.js +36 -0
  56. package/build/lib/utils/process.js.map +1 -0
  57. package/build/lib/utils/types.d.ts +4 -0
  58. package/build/lib/utils/types.d.ts.map +1 -0
  59. package/build/lib/utils/types.js +3 -0
  60. package/build/lib/utils/types.js.map +1 -0
  61. package/build/lib/utils/xcode.d.ts +27 -0
  62. package/build/lib/utils/xcode.d.ts.map +1 -0
  63. package/build/lib/utils/xcode.js +80 -0
  64. package/build/lib/utils/xcode.js.map +1 -0
  65. package/lib/extensions/applications.ts +8 -4
  66. package/lib/extensions/biometric.ts +2 -2
  67. package/lib/extensions/keychain.ts +6 -6
  68. package/lib/extensions/permissions.ts +2 -0
  69. package/lib/extensions/settings.ts +7 -8
  70. package/lib/simulator-xcode-14.ts +27 -22
  71. package/lib/simulator-xcode-15.ts +4 -15
  72. package/lib/simulator-xcode-27.ts +9 -0
  73. package/lib/simulator.ts +14 -10
  74. package/lib/utils/constants.ts +6 -0
  75. package/lib/{defaults-utils.ts → utils/defaults.ts} +7 -5
  76. package/lib/utils/devices.ts +25 -0
  77. package/lib/utils/get-devices.ts +10 -0
  78. package/lib/utils/index.ts +20 -0
  79. package/lib/utils/lifecycle.ts +78 -0
  80. package/lib/utils/process.ts +31 -0
  81. package/lib/utils/types.ts +3 -0
  82. package/lib/utils/xcode.ts +86 -0
  83. package/package.json +6 -2
  84. package/build/lib/defaults-utils.d.ts.map +0 -1
  85. package/build/lib/defaults-utils.js.map +0 -1
  86. package/build/lib/utils.d.ts +0 -52
  87. package/build/lib/utils.d.ts.map +0 -1
  88. package/build/lib/utils.js +0 -227
  89. package/build/lib/utils.js.map +0 -1
  90. package/lib/utils.ts +0 -205
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getDeveloperRoot = getDeveloperRoot;
7
+ exports.getUiClientAppPath = getUiClientAppPath;
8
+ exports.assertXcodeVersion = assertXcodeVersion;
9
+ exports.readBundleIdFromPlist = readBundleIdFromPlist;
10
+ const support_1 = require("@appium/support");
11
+ const teen_process_1 = require("teen_process");
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ const constants_1 = require("./constants");
14
+ /**
15
+ * @returns Promise that resolves to the developer root path.
16
+ */
17
+ async function getDeveloperRoot() {
18
+ const { stdout } = await (0, teen_process_1.exec)('xcode-select', ['-p']);
19
+ return stdout.trim();
20
+ }
21
+ /**
22
+ * @param bundleId - The bundle identifier of the Simulator UI client.
23
+ * @param xcodeVersion - The active Xcode version.
24
+ * @returns The full path to the UI client app in the active Xcode installation.
25
+ * @throws {Error} If no matching app is found in the active Xcode folder.
26
+ */
27
+ async function getUiClientAppPath(bundleId, xcodeVersion) {
28
+ const devRoot = await getDeveloperRoot();
29
+ const applicationsDir = xcodeVersion.major >= constants_1.MIN_DEVICE_HUB_XCODE_VERSION
30
+ ? node_path_1.default.resolve(devRoot, '..', 'Applications')
31
+ : node_path_1.default.resolve(devRoot, 'Applications');
32
+ if (await support_1.fs.exists(applicationsDir)) {
33
+ const appPaths = (await support_1.fs.readdir(applicationsDir))
34
+ .filter((entry) => entry.endsWith('.app'))
35
+ .map((entry) => node_path_1.default.resolve(applicationsDir, entry));
36
+ const apps = await Promise.all(appPaths.map(async (appPath) => ({
37
+ appPath,
38
+ bundleId: await readBundleIdFromPlist(node_path_1.default.resolve(appPath, 'Contents', 'Info.plist')),
39
+ })));
40
+ const match = apps.find((app) => app.bundleId === bundleId);
41
+ if (match) {
42
+ return match.appPath;
43
+ }
44
+ }
45
+ throw new Error(`Could not find UI client app with bundle id '${bundleId}' under '${applicationsDir}' ` +
46
+ `(active Xcode developer root: ${devRoot})`);
47
+ }
48
+ /**
49
+ * Asserts that the Xcode version meets the minimum supported version requirement.
50
+ *
51
+ * @template V - The Xcode version type.
52
+ * @param xcodeVersion - The Xcode version to check.
53
+ * @returns The same Xcode version if it meets the requirement.
54
+ * @throws {Error} If the Xcode version is below the minimum supported version.
55
+ */
56
+ function assertXcodeVersion(xcodeVersion) {
57
+ if (xcodeVersion.major < constants_1.MIN_SUPPORTED_XCODE_VERSION) {
58
+ throw new Error(`Tried to use an iOS simulator with xcode version ${xcodeVersion.versionString} but only Xcode version ` +
59
+ `${constants_1.MIN_SUPPORTED_XCODE_VERSION} and up are supported`);
60
+ }
61
+ return xcodeVersion;
62
+ }
63
+ /**
64
+ * @param infoPlistPath - The full path to an Info.plist file.
65
+ * @returns The bundle identifier or null if it cannot be read.
66
+ */
67
+ async function readBundleIdFromPlist(infoPlistPath) {
68
+ try {
69
+ const { stdout } = await (0, teen_process_1.exec)('/usr/libexec/PlistBuddy', [
70
+ '-c',
71
+ 'print CFBundleIdentifier',
72
+ infoPlistPath,
73
+ ]);
74
+ return stdout.trim() || null;
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }
80
+ //# sourceMappingURL=xcode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xcode.js","sourceRoot":"","sources":["../../../lib/utils/xcode.ts"],"names":[],"mappings":";;;;;AASA,4CAGC;AAQD,gDA8BC;AAUD,gDAQC;AAMD,sDAWC;AArFD,6CAAmC;AACnC,+CAAkC;AAClC,0DAA6B;AAE7B,2CAAsF;AAEtF;;GAEG;AACI,KAAK,UAAU,gBAAgB;IACpC,MAAM,EAAC,MAAM,EAAC,GAAG,MAAM,IAAA,mBAAI,EAAC,cAAc,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IACpD,OAAO,MAAM,CAAC,IAAI,EAAE,CAAC;AACvB,CAAC;AAED;;;;;GAKG;AACI,KAAK,UAAU,kBAAkB,CACtC,QAAgB,EAChB,YAA0B;IAE1B,MAAM,OAAO,GAAG,MAAM,gBAAgB,EAAE,CAAC;IACzC,MAAM,eAAe,GACnB,YAAY,CAAC,KAAK,IAAI,wCAA4B;QAChD,CAAC,CAAC,mBAAI,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,EAAE,cAAc,CAAC;QAC7C,CAAC,CAAC,mBAAI,CAAC,OAAO,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;IAE5C,IAAI,MAAM,YAAE,CAAC,MAAM,CAAC,eAAe,CAAC,EAAE,CAAC;QACrC,MAAM,QAAQ,GAAG,CAAC,MAAM,YAAE,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;aACjD,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;aACzC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,mBAAI,CAAC,OAAO,CAAC,eAAe,EAAE,KAAK,CAAC,CAAC,CAAC;QACxD,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,GAAG,CAC5B,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC;YAC/B,OAAO;YACP,QAAQ,EAAE,MAAM,qBAAqB,CAAC,mBAAI,CAAC,OAAO,CAAC,OAAO,EAAE,UAAU,EAAE,YAAY,CAAC,CAAC;SACvF,CAAC,CAAC,CACJ,CAAC;QACF,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC;QAC5D,IAAI,KAAK,EAAE,CAAC;YACV,OAAO,KAAK,CAAC,OAAO,CAAC;QACvB,CAAC;IACH,CAAC;IAED,MAAM,IAAI,KAAK,CACb,gDAAgD,QAAQ,YAAY,eAAe,IAAI;QACrF,iCAAiC,OAAO,GAAG,CAC9C,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,kBAAkB,CAAyB,YAAe;IACxE,IAAI,YAAY,CAAC,KAAK,GAAG,uCAA2B,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CACb,oDAAoD,YAAY,CAAC,aAAa,0BAA0B;YACtG,GAAG,uCAA2B,uBAAuB,CACxD,CAAC;IACJ,CAAC;IACD,OAAO,YAAY,CAAC;AACtB,CAAC;AAED;;;GAGG;AACI,KAAK,UAAU,qBAAqB,CAAC,aAAqB;IAC/D,IAAI,CAAC;QACH,MAAM,EAAC,MAAM,EAAC,GAAG,MAAM,IAAA,mBAAI,EAAC,yBAAyB,EAAE;YACrD,IAAI;YACJ,0BAA0B;YAC1B,aAAa;SACd,CAAC,CAAC;QACH,OAAO,MAAM,CAAC,IAAI,EAAE,IAAI,IAAI,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -1,7 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import {fs, plist, util} from '@appium/support';
3
3
  import {waitForCondition} from 'asyncbox';
4
- import {isPlainObject} from '../utils';
5
4
  import type {CoreSimulator, InteractsWithApps, LaunchAppOptions} from '../types';
6
5
 
7
6
  type CoreSimulatorWithApps = CoreSimulator & InteractsWithApps;
@@ -47,10 +46,15 @@ export async function getUserInstalledBundleIdsByBundleName(
47
46
  })(),
48
47
  );
49
48
  }
50
- const bundleInfos = (await Promise.all(bundleInfoPromises)).filter(isPlainObject);
49
+ const bundleInfos = (await Promise.all(bundleInfoPromises)).filter((info) =>
50
+ util.isPlainObject(info),
51
+ );
51
52
  const bundleIds = bundleInfos
52
- .filter(({CFBundleName}) => CFBundleName === bundleName)
53
- .map(({CFBundleIdentifier}) => CFBundleIdentifier);
53
+ .filter(
54
+ ({CFBundleName, CFBundleIdentifier}) =>
55
+ CFBundleName === bundleName && typeof CFBundleIdentifier === 'string',
56
+ )
57
+ .map(({CFBundleIdentifier}) => CFBundleIdentifier as string);
54
58
  if (bundleIds.length === 0) {
55
59
  return [];
56
60
  }
@@ -1,5 +1,5 @@
1
1
  import type {CoreSimulator, SupportsBiometric} from '../types';
2
- import {escapeRegExp} from '../utils';
2
+ import {util} from '@appium/support';
3
3
 
4
4
  type CoreSimulatorWithBiometric = CoreSimulator & SupportsBiometric;
5
5
 
@@ -18,7 +18,7 @@ export async function isBiometricEnrolled(this: CoreSimulatorWithBiometric): Pro
18
18
  '-g',
19
19
  ENROLLMENT_NOTIFICATION_RECEIVER,
20
20
  ]);
21
- const match = new RegExp(`${escapeRegExp(ENROLLMENT_NOTIFICATION_RECEIVER)}\\s+([01])`).exec(
21
+ const match = new RegExp(`${util.escapeRegExp(ENROLLMENT_NOTIFICATION_RECEIVER)}\\s+([01])`).exec(
22
22
  stdout,
23
23
  );
24
24
  if (!match) {
@@ -44,6 +44,7 @@ export async function backupKeychains(this: CoreSimulatorWithKeychain): Promise<
44
44
  throw new Error(
45
45
  `Cannot create keychains backup from '${this.keychainPath}'. ` +
46
46
  `Original error: ${err.stderr || err.stdout || err.message}`,
47
+ {cause: err},
47
48
  );
48
49
  }
49
50
  await resetBackupPath(dstPath);
@@ -75,12 +76,10 @@ export async function restoreKeychains(
75
76
  );
76
77
  }
77
78
 
78
- let patterns: string[] = [];
79
- if (typeof excludePatterns === 'string') {
80
- patterns = excludePatterns.split(',').map((x) => x.trim());
81
- } else {
82
- patterns = excludePatterns;
83
- }
79
+ const patterns =
80
+ typeof excludePatterns === 'string'
81
+ ? excludePatterns.split(',').map((x) => x.trim())
82
+ : excludePatterns;
84
83
  const isServerRunning = await this.isRunning();
85
84
  let plistPath: string | undefined;
86
85
  if (isServerRunning) {
@@ -111,6 +110,7 @@ export async function restoreKeychains(
111
110
  throw new Error(
112
111
  `Cannot restore keychains from '${backupPath}'. ` +
113
112
  `Original error: ${err.stderr || err.stdout || err.message}`,
113
+ {cause: err},
114
114
  );
115
115
  }
116
116
  await fs.unlink(backupPath);
@@ -128,6 +128,7 @@ async function execSQLiteQuery(
128
128
  } catch (err: any) {
129
129
  throw new Error(
130
130
  `Cannot execute SQLite query "${query}" to '${db}'. Original error: ${err.stderr}`,
131
+ {cause: err},
131
132
  );
132
133
  }
133
134
  }
@@ -155,6 +156,7 @@ async function execWix(this: CoreSimulatorWithAppPermissions, args: string[]): P
155
156
  } catch (e: any) {
156
157
  throw new Error(
157
158
  `Cannot execute "${WIX_SIM_UTILS} ${util.quote(args)}". Original error: ${e.stderr || e.message}`,
159
+ {cause: e},
158
160
  );
159
161
  }
160
162
  }
@@ -1,9 +1,8 @@
1
- import {NSUserDefaults, generateDefaultsCommandArgs} from '../defaults-utils';
1
+ import {NSUserDefaults, generateDefaultsCommandArgs} from '../utils';
2
2
  import path from 'node:path';
3
3
  import {exec} from 'teen_process';
4
4
  import AsyncLock from 'async-lock';
5
- import {fs} from '@appium/support';
6
- import {isPlainObject} from '../utils';
5
+ import {fs, util} from '@appium/support';
7
6
  import type {
8
7
  CoreSimulator,
9
8
  HasSettings,
@@ -209,7 +208,7 @@ export async function configureLocalization(
209
208
  const {language, locale, keyboard} = opts;
210
209
  const globalPrefs: Record<string, any> = {};
211
210
  let keyboardId: string | null = null;
212
- if (isPlainObject(keyboard)) {
211
+ if (util.isPlainObject(keyboard)) {
213
212
  const {name, layout, hardware} = keyboard;
214
213
  if (!name) {
215
214
  throw new Error(`The 'keyboard' field must have a valid name set`);
@@ -223,14 +222,14 @@ export async function configureLocalization(
223
222
  }
224
223
  globalPrefs.AppleKeyboards = [keyboardId];
225
224
  }
226
- if (isPlainObject(language)) {
225
+ if (util.isPlainObject(language)) {
227
226
  const {name} = language;
228
227
  if (!name) {
229
228
  throw new Error(`The 'language' field must have a valid name set`);
230
229
  }
231
230
  globalPrefs.AppleLanguages = [name];
232
231
  }
233
- if (isPlainObject(locale)) {
232
+ if (util.isPlainObject(locale)) {
234
233
  const {name, calendar} = locale;
235
234
  if (!name) {
236
235
  throw new Error(`The 'locale' field must have a valid name set`);
@@ -375,8 +374,8 @@ export async function updatePreferences(
375
374
  if (await fs.exists(plistPath)) {
376
375
  const currentPlistContent = await defaults.asJson();
377
376
  if (
378
- isPlainObject(currentPlistContent.DevicePreferences) &&
379
- isPlainObject(currentPlistContent.DevicePreferences[udidKey])
377
+ util.isPlainObject(currentPlistContent.DevicePreferences) &&
378
+ util.isPlainObject(currentPlistContent.DevicePreferences[udidKey])
380
379
  ) {
381
380
  existingDevicePrefs = currentPlistContent.DevicePreferences[udidKey];
382
381
  }
@@ -1,12 +1,16 @@
1
1
  import {fs, timing, util} from '@appium/support';
2
2
  import {waitForCondition, retryInterval} from 'asyncbox';
3
- import {getDeveloperRoot, SIMULATOR_APP_NAME} from './utils';
3
+ import {
4
+ getDeveloperRoot,
5
+ getMacAppPidByBundleId,
6
+ getUiClientAppPath,
7
+ SIMULATOR_UI_CLIENT_BUNDLE_ID,
8
+ } from './utils';
4
9
  import {exec} from 'teen_process';
5
10
  import {log as defaultLog} from './logger';
6
11
  import EventEmitter from 'node:events';
7
12
  import AsyncLock from 'async-lock';
8
13
  import path from 'node:path';
9
- import {getPath as getXcodePath} from 'appium-xcode';
10
14
  import {Simctl} from 'node-simctl';
11
15
  import * as appExtensions from './extensions/applications';
12
16
  import * as biometricExtensions from './extensions/biometric';
@@ -37,7 +41,6 @@ import type {AppiumLogger, StringRecord} from '@appium/types';
37
41
 
38
42
  const SIMULATOR_SHUTDOWN_TIMEOUT = 15 * 1000;
39
43
  const STARTUP_LOCK = new AsyncLock();
40
- const UI_CLIENT_BUNDLE_ID = 'com.apple.iphonesimulator';
41
44
  const STARTUP_TIMEOUT_MS = 120 * 1000;
42
45
 
43
46
  export class SimulatorXcode14
@@ -110,6 +113,7 @@ export class SimulatorXcode14
110
113
  private readonly _simctl: Simctl;
111
114
  private readonly _xcodeVersion: XcodeVersion;
112
115
  private readonly _log: AppiumLogger;
116
+ private _uiClientAppPath: Promise<string> | undefined;
113
117
 
114
118
  /**
115
119
  * Constructs the object with the `udid` and version of Xcode.
@@ -174,7 +178,7 @@ export class SimulatorXcode14
174
178
  * @returns The bundle identifier of the Simulator UI client.
175
179
  */
176
180
  get uiClientBundleId(): string {
177
- return UI_CLIENT_BUNDLE_ID;
181
+ return SIMULATOR_UI_CLIENT_BUNDLE_ID;
178
182
  }
179
183
 
180
184
  /**
@@ -305,18 +309,11 @@ export class SimulatorXcode14
305
309
  * @returns The process ID or null if the UI client is not running.
306
310
  */
307
311
  async getUIClientPid(): Promise<string | null> {
308
- let stdout: string;
309
- try {
310
- ({stdout} = await exec('pgrep', ['-fn', `${SIMULATOR_APP_NAME}/Contents/MacOS/`]));
311
- } catch {
312
- return null;
312
+ const pid = await getMacAppPidByBundleId(this.uiClientBundleId);
313
+ if (pid) {
314
+ this.log.debug(`Got UI client PID: ${pid}`);
313
315
  }
314
- if (isNaN(parseInt(stdout, 10))) {
315
- return null;
316
- }
317
- stdout = stdout.trim();
318
- this.log.debug(`Got Simulator UI client PID: ${stdout}`);
319
- return stdout;
316
+ return pid;
320
317
  }
321
318
 
322
319
  /**
@@ -456,16 +453,15 @@ export class SimulatorXcode14
456
453
  ...opts,
457
454
  };
458
455
 
459
- const simulatorApp = path.resolve(await getXcodePath(), 'Applications', SIMULATOR_APP_NAME);
460
- const args = ['-Fn', simulatorApp];
461
- this.log.info(`Starting Simulator UI: ${util.quote(['open', ...args])}`);
456
+ const uiClientApp = await this._getUiClientAppPath();
457
+ const args = ['-Fn', uiClientApp];
458
+ this.log.info(`Starting UI client: ${util.quote(['open', ...args])}`);
462
459
  try {
463
460
  await exec('open', args, {timeout: startUiOpts.startupTimeout});
464
461
  } catch (err: any) {
465
462
  throw new Error(
466
- `Got an unexpected error while opening Simulator UI: ` + err.stderr ||
467
- err.stdout ||
468
- err.message,
463
+ `Got an unexpected error while opening UI client: ${err.stderr || err.stdout || err.message}`,
464
+ {cause: err},
469
465
  );
470
466
  }
471
467
  }
@@ -580,7 +576,9 @@ export class SimulatorXcode14
580
576
  if (e.code === 1) {
581
577
  return false;
582
578
  }
583
- throw new Error(`Cannot kill the Simulator UI client. Original error: ${e.message}`);
579
+ throw new Error(`Cannot kill the Simulator UI client. Original error: ${e.message}`, {
580
+ cause: e,
581
+ });
584
582
  }
585
583
  }
586
584
 
@@ -665,4 +663,11 @@ export class SimulatorXcode14
665
663
  'LaunchDaemons',
666
664
  );
667
665
  }
666
+
667
+ private async _getUiClientAppPath(): Promise<string> {
668
+ if (!this._uiClientAppPath) {
669
+ this._uiClientAppPath = getUiClientAppPath(this.uiClientBundleId, this.xcodeVersion);
670
+ }
671
+ return this._uiClientAppPath;
672
+ }
668
673
  }
@@ -1,6 +1,6 @@
1
1
  import {fs} from '@appium/support';
2
- import {exec} from 'teen_process';
3
2
  import path from 'node:path';
3
+ import {readBundleIdFromPlist} from './utils';
4
4
  import {SimulatorXcode14} from './simulator-xcode-14';
5
5
 
6
6
  export class SimulatorXcode15 extends SimulatorXcode14 {
@@ -114,23 +114,12 @@ export class SimulatorXcode15 extends SimulatorXcode14 {
114
114
  }
115
115
 
116
116
  const appsRoot = path.resolve(await this._getSystemRoot(), 'Applications');
117
- const fetchBundleId = async (appRoot: string): Promise<string | null> => {
118
- const infoPlistPath = path.resolve(appRoot, 'Info.plist');
119
- try {
120
- const {stdout} = await exec('/usr/libexec/PlistBuddy', [
121
- '-c',
122
- 'print CFBundleIdentifier',
123
- infoPlistPath,
124
- ]);
125
- return stdout.trim();
126
- } catch {
127
- return null;
128
- }
129
- };
130
117
  const allApps = (await fs.readdir(appsRoot))
131
118
  .filter((x) => x.endsWith('.app'))
132
119
  .map((x) => path.join(appsRoot, x));
133
- const bundleIds = await Promise.all(allApps.map(fetchBundleId));
120
+ const bundleIds = await Promise.all(
121
+ allApps.map((appRoot) => readBundleIdFromPlist(path.resolve(appRoot, 'Info.plist'))),
122
+ );
134
123
  this._systemAppBundleIds = new Set(bundleIds.filter((x): x is string => x !== null));
135
124
  return this._systemAppBundleIds;
136
125
  }
@@ -0,0 +1,9 @@
1
+ import {DEVICE_HUB_UI_CLIENT_BUNDLE_ID} from './utils';
2
+ import {SimulatorXcode15} from './simulator-xcode-15';
3
+
4
+ export class SimulatorXcode27 extends SimulatorXcode15 {
5
+ /** @inheritdoc */
6
+ override get uiClientBundleId(): string {
7
+ return DEVICE_HUB_UI_CLIENT_BUNDLE_ID;
8
+ }
9
+ }
package/lib/simulator.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import {SimulatorXcode14} from './simulator-xcode-14';
2
2
  import {SimulatorXcode15} from './simulator-xcode-15';
3
- import {getSimulatorInfo, assertXcodeVersion, MIN_SUPPORTED_XCODE_VERSION} from './utils';
3
+ import {SimulatorXcode27} from './simulator-xcode-27';
4
+ import {
5
+ assertXcodeVersion,
6
+ getSimulatorInfo,
7
+ MIN_DEVICE_HUB_XCODE_VERSION,
8
+ MIN_SUPPORTED_XCODE_VERSION,
9
+ } from './utils';
4
10
  import * as xcode from 'appium-xcode';
5
11
  import {log} from './logger';
6
12
  import type {Simulator, SimulatorLookupOptions} from './types';
@@ -38,15 +44,13 @@ export async function getSimulator(
38
44
  (logger ?? log).info(
39
45
  `Constructing ${platform} simulator for Xcode version ${xcodeVersion.versionString} with udid '${udid}'`,
40
46
  );
41
- let SimClass: typeof SimulatorXcode14 | typeof SimulatorXcode15;
42
- switch (xcodeVersion.major) {
43
- case MIN_SUPPORTED_XCODE_VERSION:
44
- SimClass = SimulatorXcode14;
45
- break;
46
- case 15:
47
- default:
48
- SimClass = SimulatorXcode15;
49
- break;
47
+ let SimClass: typeof SimulatorXcode14 | typeof SimulatorXcode15 | typeof SimulatorXcode27;
48
+ if (xcodeVersion.major === MIN_SUPPORTED_XCODE_VERSION) {
49
+ SimClass = SimulatorXcode14;
50
+ } else if (xcodeVersion.major >= MIN_DEVICE_HUB_XCODE_VERSION) {
51
+ SimClass = SimulatorXcode27;
52
+ } else {
53
+ SimClass = SimulatorXcode15;
50
54
  }
51
55
 
52
56
  const result = new SimClass(udid, xcodeVersion, logger);
@@ -0,0 +1,6 @@
1
+ export const SAFARI_STARTUP_TIMEOUT_MS = 25 * 1000;
2
+ export const MOBILE_SAFARI_BUNDLE_ID = 'com.apple.mobilesafari';
3
+ export const SIMULATOR_UI_CLIENT_BUNDLE_ID = 'com.apple.iphonesimulator';
4
+ export const DEVICE_HUB_UI_CLIENT_BUNDLE_ID = 'com.apple.dt.Devices';
5
+ export const MIN_SUPPORTED_XCODE_VERSION = 14;
6
+ export const MIN_DEVICE_HUB_XCODE_VERSION = 27;
@@ -1,7 +1,7 @@
1
1
  import {DOMParser, XMLSerializer, type Document, type Element} from '@xmldom/xmldom';
2
2
  import {exec} from 'teen_process';
3
- import {log} from './logger';
4
- import {isPlainObject} from './utils';
3
+ import {log} from '../logger';
4
+ import {util} from '@appium/support';
5
5
 
6
6
  export class NSUserDefaults {
7
7
  plist: string;
@@ -24,6 +24,7 @@ export class NSUserDefaults {
24
24
  } catch (e: any) {
25
25
  throw new Error(
26
26
  `'${this.plist}' cannot be converted to JSON. Original error: ${e.stderr || e.message}`,
27
+ {cause: e},
27
28
  );
28
29
  }
29
30
  }
@@ -40,7 +41,7 @@ export class NSUserDefaults {
40
41
  * @throws {Error} If there was an error while updating the plist
41
42
  */
42
43
  async update(valuesMap: Record<string, any>): Promise<void> {
43
- if (!isPlainObject(valuesMap)) {
44
+ if (!util.isPlainObject(valuesMap)) {
44
45
  throw new TypeError(`plist values must be a map. '${valuesMap}' is given instead`);
45
46
  }
46
47
  if (Object.keys(valuesMap).length === 0) {
@@ -55,6 +56,7 @@ export class NSUserDefaults {
55
56
  } catch (e: any) {
56
57
  throw new Error(
57
58
  `Could not write defaults into '${this.plist}'. Original error: ${e.stderr || e.message}`,
59
+ {cause: e},
58
60
  );
59
61
  }
60
62
  }
@@ -75,7 +77,7 @@ export class NSUserDefaults {
75
77
  export function toXmlArg(value: any, serialize: boolean = true): string | Element {
76
78
  let xmlDoc: Document | null = null;
77
79
 
78
- if (isPlainObject(value)) {
80
+ if (util.isPlainObject(value)) {
79
81
  xmlDoc = new DOMParser().parseFromString('<dict></dict>', 'text/xml');
80
82
  const documentElement = requireDocumentElement(xmlDoc);
81
83
  for (const [subKey, subValue] of Object.entries(value)) {
@@ -136,7 +138,7 @@ export function generateDefaultsCommandArgs(
136
138
  const resultArgs: string[][] = [];
137
139
  for (const [key, value] of Object.entries(valuesMap)) {
138
140
  try {
139
- if (!replace && isPlainObject(value)) {
141
+ if (!replace && util.isPlainObject(value)) {
140
142
  const dictArgs = [key, '-dict-add'];
141
143
  for (const [subKey, subValue] of Object.entries(value)) {
142
144
  dictArgs.push(subKey, toXmlArg(subValue) as string);
@@ -0,0 +1,25 @@
1
+ import type {SimulatorInfoOptions} from './types';
2
+ import {getDevices} from './get-devices';
3
+
4
+ /**
5
+ * @param udid - The simulator UDID.
6
+ * @param opts - Options including devicesSetPath.
7
+ * @returns Promise that resolves to simulator info or undefined if not found.
8
+ */
9
+ export async function getSimulatorInfo(
10
+ udid: string,
11
+ opts: SimulatorInfoOptions = {},
12
+ ): Promise<any> {
13
+ const {devicesSetPath} = opts;
14
+ // see the README for github.com/appium/node-simctl for example output of getDevices()
15
+ const devices = Object.values(await getDevices({devicesSetPath})).flat();
16
+ return devices.find((sim: any) => sim.udid === udid);
17
+ }
18
+
19
+ /**
20
+ * @param udid - The simulator UDID.
21
+ * @returns Promise that resolves to true if simulator exists, false otherwise.
22
+ */
23
+ export async function simExists(udid: string): Promise<boolean> {
24
+ return !!(await getSimulatorInfo(udid));
25
+ }
@@ -0,0 +1,10 @@
1
+ import {Simctl} from 'node-simctl';
2
+ import type {StringRecord} from '@appium/types';
3
+
4
+ /**
5
+ * @param simctlOpts - Optional simctl options
6
+ * @returns Promise that resolves to a record of devices grouped by SDK version
7
+ */
8
+ export async function getDevices(simctlOpts?: StringRecord): Promise<Record<string, any[]>> {
9
+ return await new Simctl(simctlOpts).getDevices();
10
+ }
@@ -0,0 +1,20 @@
1
+ export {
2
+ SAFARI_STARTUP_TIMEOUT_MS,
3
+ MOBILE_SAFARI_BUNDLE_ID,
4
+ SIMULATOR_UI_CLIENT_BUNDLE_ID,
5
+ DEVICE_HUB_UI_CLIENT_BUNDLE_ID,
6
+ MIN_SUPPORTED_XCODE_VERSION,
7
+ MIN_DEVICE_HUB_XCODE_VERSION,
8
+ } from './constants';
9
+ export type {SimulatorInfoOptions} from './types';
10
+ export {NSUserDefaults, toXmlArg, generateDefaultsCommandArgs} from './defaults';
11
+ export {getDevices} from './get-devices';
12
+ export {getSimulatorInfo, simExists} from './devices';
13
+ export {getMacAppPidByBundleId} from './process';
14
+ export {
15
+ assertXcodeVersion,
16
+ getDeveloperRoot,
17
+ getUiClientAppPath,
18
+ readBundleIdFromPlist,
19
+ } from './xcode';
20
+ export {killAllSimulators} from './lifecycle';
@@ -0,0 +1,78 @@
1
+ import {log} from '../logger';
2
+ import {exec, type ExecError} from 'teen_process';
3
+ import {waitForCondition} from 'asyncbox';
4
+ import {getVersion} from 'appium-xcode';
5
+ import {
6
+ DEVICE_HUB_UI_CLIENT_BUNDLE_ID,
7
+ MIN_DEVICE_HUB_XCODE_VERSION,
8
+ SIMULATOR_UI_CLIENT_BUNDLE_ID,
9
+ } from './constants';
10
+ import {getDevices} from './get-devices';
11
+ import {getMacAppPidByBundleId, killMacAppByBundleId} from './process';
12
+
13
+ const DEFAULT_SIM_SHUTDOWN_TIMEOUT_MS = 60000;
14
+
15
+ /**
16
+ * @param timeout - Timeout in milliseconds (default: DEFAULT_SIM_SHUTDOWN_TIMEOUT_MS).
17
+ * @returns Promise that resolves when all simulators are killed.
18
+ */
19
+ export async function killAllSimulators(
20
+ timeout: number = DEFAULT_SIM_SHUTDOWN_TIMEOUT_MS,
21
+ ): Promise<void> {
22
+ log.debug('Killing all iOS Simulators');
23
+ const xcodeVersion = await getVersion(true);
24
+ const uiClientBundleId =
25
+ xcodeVersion.major >= MIN_DEVICE_HUB_XCODE_VERSION
26
+ ? DEVICE_HUB_UI_CLIENT_BUNDLE_ID
27
+ : SIMULATOR_UI_CLIENT_BUNDLE_ID;
28
+
29
+ const startedMs = performance.now();
30
+ try {
31
+ await exec('xcrun', ['simctl', 'shutdown', 'all'], {timeout});
32
+ } catch (err: unknown) {
33
+ log.debug(
34
+ `Failed to shutdown all simulators: ${(err as ExecError).stderr || (err as Error).message}`,
35
+ );
36
+ }
37
+
38
+ const uiClientPid = await getMacAppPidByBundleId(uiClientBundleId);
39
+ if (uiClientPid) {
40
+ log.debug(`Killing UI client '${uiClientBundleId}' (pid ${uiClientPid})`);
41
+ await killMacAppByBundleId(uiClientBundleId);
42
+ } else {
43
+ log.debug(`UI client '${uiClientBundleId}' is not running`);
44
+ }
45
+
46
+ try {
47
+ await waitForCondition(allSimsAreDown, {
48
+ waitMs: Math.max(1000, startedMs + timeout - performance.now()),
49
+ intervalMs: 200,
50
+ });
51
+ } catch (err) {
52
+ const remainingDevices = await getNonShutdownDeviceDescriptions();
53
+ const message =
54
+ remainingDevices.length > 0
55
+ ? `The following devices are still not in the correct state after ${timeout} ms:\n` +
56
+ remainingDevices.map((device) => ` ${device}`).join('\n')
57
+ : `Timed out after ${timeout} ms waiting for all simulators to shut down`;
58
+ throw new Error(message, {cause: err});
59
+ }
60
+ }
61
+
62
+ async function allSimsAreDown(): Promise<boolean> {
63
+ try {
64
+ return (await getNonShutdownDeviceDescriptions()).length === 0;
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ async function getNonShutdownDeviceDescriptions(): Promise<string[]> {
71
+ const devices = Object.values(await getDevices()).flat();
72
+ return devices
73
+ .filter((sim) => !['shutdown', 'unavailable', 'disconnected'].includes(sim.state.toLowerCase()))
74
+ .map(
75
+ (sim) =>
76
+ `${sim.name} (${sim.sdk}, udid: ${sim.udid}) is still in state '${sim.state.toLowerCase()}'`,
77
+ );
78
+ }
@@ -0,0 +1,31 @@
1
+ import {log} from '../logger';
2
+ import {exec} from 'teen_process';
3
+
4
+ /**
5
+ * @param bundleId - The bundle identifier of a running macOS application.
6
+ * @returns The process ID or null if the application is not running.
7
+ */
8
+ export async function getMacAppPidByBundleId(bundleId: string): Promise<string | null> {
9
+ let stdout: string;
10
+ try {
11
+ ({stdout} = await exec('lsappinfo', ['info', '-only', 'pid', bundleId]));
12
+ } catch {
13
+ return null;
14
+ }
15
+ const match = stdout.trim().match(/"pid"=(\d+)/);
16
+ return match?.[1] ?? null;
17
+ }
18
+
19
+ /**
20
+ * @param bundleId - The bundle identifier of a running macOS application.
21
+ * @returns True if the kill command succeeded.
22
+ */
23
+ export async function killMacAppByBundleId(bundleId: string): Promise<boolean> {
24
+ try {
25
+ await exec('lsappinfo', ['kill', '-hard', bundleId]);
26
+ return true;
27
+ } catch (e: any) {
28
+ log.debug(`Could not kill '${bundleId}' via lsappinfo: ${e.stderr || e.message}`);
29
+ return false;
30
+ }
31
+ }
@@ -0,0 +1,3 @@
1
+ export interface SimulatorInfoOptions {
2
+ devicesSetPath?: string | null;
3
+ }