@appium/coresim 1.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 (138) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +66 -0
  3. package/binding.gyp +53 -0
  4. package/lib/src/commands/app.d.ts +88 -0
  5. package/lib/src/commands/app.d.ts.map +1 -0
  6. package/lib/src/commands/app.js +133 -0
  7. package/lib/src/commands/app.js.map +1 -0
  8. package/lib/src/commands/biometric.d.ts +33 -0
  9. package/lib/src/commands/biometric.d.ts.map +1 -0
  10. package/lib/src/commands/biometric.js +44 -0
  11. package/lib/src/commands/biometric.js.map +1 -0
  12. package/lib/src/commands/darwin-notification.d.ts +36 -0
  13. package/lib/src/commands/darwin-notification.d.ts.map +1 -0
  14. package/lib/src/commands/darwin-notification.js +35 -0
  15. package/lib/src/commands/darwin-notification.js.map +1 -0
  16. package/lib/src/commands/interaction.d.ts +48 -0
  17. package/lib/src/commands/interaction.d.ts.map +1 -0
  18. package/lib/src/commands/interaction.js +48 -0
  19. package/lib/src/commands/interaction.js.map +1 -0
  20. package/lib/src/commands/keychain.d.ts +25 -0
  21. package/lib/src/commands/keychain.d.ts.map +1 -0
  22. package/lib/src/commands/keychain.js +52 -0
  23. package/lib/src/commands/keychain.js.map +1 -0
  24. package/lib/src/commands/lifecycle.d.ts +101 -0
  25. package/lib/src/commands/lifecycle.d.ts.map +1 -0
  26. package/lib/src/commands/lifecycle.js +165 -0
  27. package/lib/src/commands/lifecycle.js.map +1 -0
  28. package/lib/src/commands/media.d.ts +28 -0
  29. package/lib/src/commands/media.d.ts.map +1 -0
  30. package/lib/src/commands/media.js +27 -0
  31. package/lib/src/commands/media.js.map +1 -0
  32. package/lib/src/commands/misc.d.ts +15 -0
  33. package/lib/src/commands/misc.d.ts.map +1 -0
  34. package/lib/src/commands/misc.js +12 -0
  35. package/lib/src/commands/misc.js.map +1 -0
  36. package/lib/src/commands/pasteboard.d.ts +24 -0
  37. package/lib/src/commands/pasteboard.d.ts.map +1 -0
  38. package/lib/src/commands/pasteboard.js +22 -0
  39. package/lib/src/commands/pasteboard.js.map +1 -0
  40. package/lib/src/commands/permissions.d.ts +48 -0
  41. package/lib/src/commands/permissions.d.ts.map +1 -0
  42. package/lib/src/commands/permissions.js +84 -0
  43. package/lib/src/commands/permissions.js.map +1 -0
  44. package/lib/src/commands/process.d.ts +16 -0
  45. package/lib/src/commands/process.d.ts.map +1 -0
  46. package/lib/src/commands/process.js +68 -0
  47. package/lib/src/commands/process.js.map +1 -0
  48. package/lib/src/commands/screenshot.d.ts +29 -0
  49. package/lib/src/commands/screenshot.d.ts.map +1 -0
  50. package/lib/src/commands/screenshot.js +30 -0
  51. package/lib/src/commands/screenshot.js.map +1 -0
  52. package/lib/src/commands/spawn.d.ts +48 -0
  53. package/lib/src/commands/spawn.d.ts.map +1 -0
  54. package/lib/src/commands/spawn.js +108 -0
  55. package/lib/src/commands/spawn.js.map +1 -0
  56. package/lib/src/commands/ui.d.ts +42 -0
  57. package/lib/src/commands/ui.d.ts.map +1 -0
  58. package/lib/src/commands/ui.js +44 -0
  59. package/lib/src/commands/ui.js.map +1 -0
  60. package/lib/src/commands/webinspector.d.ts +19 -0
  61. package/lib/src/commands/webinspector.d.ts.map +1 -0
  62. package/lib/src/commands/webinspector.js +16 -0
  63. package/lib/src/commands/webinspector.js.map +1 -0
  64. package/lib/src/errors.d.ts +51 -0
  65. package/lib/src/errors.d.ts.map +1 -0
  66. package/lib/src/errors.js +82 -0
  67. package/lib/src/errors.js.map +1 -0
  68. package/lib/src/index.d.ts +7 -0
  69. package/lib/src/index.d.ts.map +1 -0
  70. package/lib/src/index.js +5 -0
  71. package/lib/src/index.js.map +1 -0
  72. package/lib/src/native-simctl.d.ts +62 -0
  73. package/lib/src/native-simctl.d.ts.map +1 -0
  74. package/lib/src/native-simctl.js +221 -0
  75. package/lib/src/native-simctl.js.map +1 -0
  76. package/lib/src/types.d.ts +267 -0
  77. package/lib/src/types.d.ts.map +1 -0
  78. package/lib/src/types.js +29 -0
  79. package/lib/src/types.js.map +1 -0
  80. package/lib/src/utils/index.d.ts +3 -0
  81. package/lib/src/utils/index.d.ts.map +1 -0
  82. package/lib/src/utils/index.js +3 -0
  83. package/lib/src/utils/index.js.map +1 -0
  84. package/lib/src/utils/pkg-root.d.ts +6 -0
  85. package/lib/src/utils/pkg-root.d.ts.map +1 -0
  86. package/lib/src/utils/pkg-root.js +12 -0
  87. package/lib/src/utils/pkg-root.js.map +1 -0
  88. package/lib/src/utils/run-catching.d.ts +3 -0
  89. package/lib/src/utils/run-catching.d.ts.map +1 -0
  90. package/lib/src/utils/run-catching.js +11 -0
  91. package/lib/src/utils/run-catching.js.map +1 -0
  92. package/package.json +73 -0
  93. package/scripts/install.mjs +22 -0
  94. package/src/commands/app.ts +186 -0
  95. package/src/commands/biometric.ts +69 -0
  96. package/src/commands/darwin-notification.ts +51 -0
  97. package/src/commands/interaction.ts +74 -0
  98. package/src/commands/keychain.ts +63 -0
  99. package/src/commands/lifecycle.ts +210 -0
  100. package/src/commands/media.ts +38 -0
  101. package/src/commands/misc.ts +20 -0
  102. package/src/commands/pasteboard.ts +31 -0
  103. package/src/commands/permissions.ts +122 -0
  104. package/src/commands/process.ts +80 -0
  105. package/src/commands/screenshot.ts +46 -0
  106. package/src/commands/spawn.ts +133 -0
  107. package/src/commands/ui.ts +61 -0
  108. package/src/commands/webinspector.ts +23 -0
  109. package/src/coresim.mm +1066 -0
  110. package/src/errors.ts +93 -0
  111. package/src/index.ts +23 -0
  112. package/src/native/async_bridge.h +164 -0
  113. package/src/native/nserror_bridge.h +64 -0
  114. package/src/native/nserror_bridge.mm +30 -0
  115. package/src/native/objc_runtime.h +45 -0
  116. package/src/native/objc_runtime.mm +76 -0
  117. package/src/native/safe_dispatch.h +38 -0
  118. package/src/native/sim_device.h +143 -0
  119. package/src/native/sim_device.mm +355 -0
  120. package/src/native/sim_device_set.h +16 -0
  121. package/src/native/sim_device_set.mm +40 -0
  122. package/src/native/sim_pasteboard.h +16 -0
  123. package/src/native/sim_pasteboard.mm +295 -0
  124. package/src/native/sim_process.h +13 -0
  125. package/src/native/sim_process.mm +139 -0
  126. package/src/native/sim_screenshot.h +28 -0
  127. package/src/native/sim_screenshot.mm +226 -0
  128. package/src/native/sim_service_context.h +42 -0
  129. package/src/native/sim_service_context.mm +92 -0
  130. package/src/native/tcc_privacy.h +35 -0
  131. package/src/native/tcc_privacy.mm +256 -0
  132. package/src/native/value_bridge.h +18 -0
  133. package/src/native/value_bridge.mm +111 -0
  134. package/src/native-simctl.ts +288 -0
  135. package/src/types.ts +302 -0
  136. package/src/utils/index.ts +2 -0
  137. package/src/utils/pkg-root.ts +14 -0
  138. package/src/utils/run-catching.ts +10 -0
@@ -0,0 +1,186 @@
1
+ import {fileURLToPath} from 'node:url';
2
+
3
+ import type {NativeSimctl} from '../native-simctl.js';
4
+ import {runCatchingAsync} from '../utils/index.js';
5
+
6
+ declare module '../native-simctl.js' {
7
+ interface NativeSimctl {
8
+ installApp(udid: string, appPath: string, options?: Record<string, unknown>): Promise<void>;
9
+ removeApp(udid: string, bundleId: string, options?: Record<string, unknown>): Promise<void>;
10
+ launchApp(udid: string, bundleId: string, options?: Record<string, unknown>): Promise<number>;
11
+ terminateApp(udid: string, bundleId: string): Promise<void>;
12
+ isAppInstalled(udid: string, bundleId: string): Promise<boolean>;
13
+ appInfo(udid: string, bundleId: string): Promise<Record<string, unknown>>;
14
+ installedApps(udid: string): Promise<Record<string, unknown>>;
15
+ getAppContainer(udid: string, bundleId: string, containerType?: AppContainerType | string): Promise<string>;
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Which of an app's on-disk containers {@link getAppContainer} should resolve — `'app'` (the
21
+ * `.app` bundle itself), `'data'` (its data container), `'groups'` (its sole App Group container,
22
+ * if it has exactly one), or any other string naming a specific App Group identifier.
23
+ */
24
+ export type AppContainerType = 'app' | 'data' | 'groups';
25
+
26
+ /**
27
+ * Installs an `.app` bundle onto the given device.
28
+ *
29
+ * @param udid — UDID of the target device
30
+ * @param appPath — path to the `.app` bundle on disk
31
+ * @param options — passed through to `installApplication:withOptions:error:`
32
+ */
33
+ export async function installApp(
34
+ this: NativeSimctl,
35
+ udid: string,
36
+ appPath: string,
37
+ options: Record<string, unknown> = {},
38
+ ): Promise<void> {
39
+ return runCatchingAsync(async () => (await this._findDevice(udid)).installApp(appPath, options));
40
+ }
41
+
42
+ /**
43
+ * Uninstalls an app from the given device.
44
+ *
45
+ * @param udid — UDID of the target device
46
+ * @param bundleId — bundle identifier of the app to remove
47
+ * @param options — passed through to `uninstallApplication:withOptions:error:`
48
+ */
49
+ export async function removeApp(
50
+ this: NativeSimctl,
51
+ udid: string,
52
+ bundleId: string,
53
+ options: Record<string, unknown> = {},
54
+ ): Promise<void> {
55
+ return runCatchingAsync(async () => (await this._findDevice(udid)).uninstallApp(bundleId, options));
56
+ }
57
+
58
+ /**
59
+ * Launches an installed app on the given device.
60
+ *
61
+ * @param udid — UDID of the target device
62
+ * @param bundleId — bundle identifier of the app to launch
63
+ * @param options — passed through to `launchApplicationWithID:options:error:`
64
+ * @returns the launched process's pid
65
+ */
66
+ export async function launchApp(
67
+ this: NativeSimctl,
68
+ udid: string,
69
+ bundleId: string,
70
+ options: Record<string, unknown> = {},
71
+ ): Promise<number> {
72
+ return runCatchingAsync(async () => (await this._findDevice(udid)).launchApp(bundleId, options));
73
+ }
74
+
75
+ /**
76
+ * @param udid — UDID of the target device
77
+ * @param bundleId — bundle identifier of the app to terminate
78
+ */
79
+ export async function terminateApp(this: NativeSimctl, udid: string, bundleId: string): Promise<void> {
80
+ return runCatchingAsync(async () => (await this._findDevice(udid)).terminateApp(bundleId));
81
+ }
82
+
83
+ /**
84
+ * @param udid — UDID of the device to check
85
+ * @param bundleId — bundle identifier to look up
86
+ * @returns whether an app with that bundle identifier is installed
87
+ */
88
+ export async function isAppInstalled(this: NativeSimctl, udid: string, bundleId: string): Promise<boolean> {
89
+ return runCatchingAsync(async () => bundleId in (await (await this._findDevice(udid)).installedApps()));
90
+ }
91
+
92
+ /**
93
+ * @param udid — UDID of the device to read from
94
+ * @param bundleId — bundle identifier of the installed app
95
+ * @returns the app's properties, as reported by `propertiesOfApplication:`
96
+ */
97
+ export async function appInfo(this: NativeSimctl, udid: string, bundleId: string): Promise<Record<string, unknown>> {
98
+ return runCatchingAsync(async () => (await this._findDevice(udid)).propertiesOfApplication(bundleId));
99
+ }
100
+
101
+ /**
102
+ * @param udid — UDID of the device to read from
103
+ * @returns every installed app's properties, keyed by bundle identifier
104
+ */
105
+ export async function installedApps(this: NativeSimctl, udid: string): Promise<Record<string, unknown>> {
106
+ return runCatchingAsync(async () => (await this._findDevice(udid)).installedApps());
107
+ }
108
+
109
+ /** Converts a `file://` URL string (as `appInfo`'s container fields report) to a plain fs path. */
110
+ function toFsPath(fileUrl: unknown): string | undefined {
111
+ if (typeof fileUrl !== 'string') {
112
+ return undefined;
113
+ }
114
+ return fileURLToPath(fileUrl).replace(/\/$/, '');
115
+ }
116
+
117
+ /**
118
+ * Resolves the full filesystem path to one of an installed app's on-disk containers — the same
119
+ * paths {@link appInfo}'s `Path`/`DataContainer`/`GroupContainers` fields already carry, just
120
+ * picked out and normalized to a plain path (no new native call).
121
+ *
122
+ * @example
123
+ * // Resolve a specific App Group container by its identifier (see appInfo's GroupContainers
124
+ * // field, or catch the error thrown below, to discover which identifiers an app has)
125
+ * const groupPath = await sim.getAppContainer(udid, bundleId, 'group.com.example.myapp');
126
+ *
127
+ * @param udid — UDID of the (booted) device to read from
128
+ * @param bundleId — bundle identifier of the installed app
129
+ * @param containerType — see {@link AppContainerType}; defaults to `'app'`
130
+ * @returns the resolved container's path
131
+ * @throws if the requested container doesn't exist (e.g. `'data'` before the device has booted
132
+ * once with the app installed), if `'groups'` is ambiguous (more than one App Group container —
133
+ * pass the specific group identifier instead), or if a given group identifier doesn't match any
134
+ * of the app's App Group containers — in both of the latter cases, the error message lists the
135
+ * app's actual App Group identifiers
136
+ */
137
+ export async function getAppContainer(
138
+ this: NativeSimctl,
139
+ udid: string,
140
+ bundleId: string,
141
+ containerType: AppContainerType | string = 'app',
142
+ ): Promise<string> {
143
+ return runCatchingAsync(async () => {
144
+ const info = await this.appInfo(udid, bundleId);
145
+ if (containerType === 'app') {
146
+ const path = info.Path;
147
+ if (typeof path !== 'string') {
148
+ throw new Error(`No app bundle path was reported for '${bundleId}'`);
149
+ }
150
+ return path;
151
+ }
152
+ if (containerType === 'data') {
153
+ const path = toFsPath(info.DataContainer);
154
+ if (!path) {
155
+ throw new Error(`No data container was found for '${bundleId}' — has the device been booted since install?`);
156
+ }
157
+ return path;
158
+ }
159
+ const groupContainers = (info.GroupContainers ?? {}) as Record<string, unknown>;
160
+ if (containerType === 'groups') {
161
+ const entries = Object.entries(groupContainers);
162
+ if (entries.length === 0) {
163
+ throw new Error(`'${bundleId}' has no App Group containers`);
164
+ }
165
+ if (entries.length > 1) {
166
+ throw new Error(
167
+ `'${bundleId}' has multiple App Group containers (${Object.keys(groupContainers).join(', ')}) — ` +
168
+ `specify one by its group identifier instead of 'groups'`,
169
+ );
170
+ }
171
+ const path = toFsPath(entries[0][1]);
172
+ if (!path) {
173
+ throw new Error(`'${bundleId}''s App Group container has no reported path`);
174
+ }
175
+ return path;
176
+ }
177
+ const path = toFsPath(groupContainers[containerType]);
178
+ if (!path) {
179
+ const available = Object.keys(groupContainers);
180
+ const availability =
181
+ available.length > 0 ? `available: ${available.join(', ')}` : 'it has no App Group containers at all';
182
+ throw new Error(`'${bundleId}' has no App Group container with identifier '${containerType}' — ${availability}`);
183
+ }
184
+ return path;
185
+ });
186
+ }
@@ -0,0 +1,69 @@
1
+ import type {NativeSimctl} from '../native-simctl.js';
2
+
3
+ declare module '../native-simctl.js' {
4
+ interface NativeSimctl {
5
+ isBiometricEnrolled(udid: string): Promise<boolean>;
6
+ enrollBiometric(udid: string, isEnabled?: boolean): Promise<void>;
7
+ sendBiometricMatch(udid: string, shouldMatch?: boolean, biometricName?: BiometricName): Promise<void>;
8
+ }
9
+ }
10
+
11
+ /** A simulated biometric sensor — Face ID is only available since iOS 11. */
12
+ export type BiometricName = 'touchId' | 'faceId';
13
+
14
+ // Simulator.app's own Features > Face ID/Touch ID menu drives the same two Darwin notifications
15
+ // (see darwin-notification.ts) rather than any dedicated CoreSimulator API — no native call needed.
16
+ const ENROLLMENT_NOTIFICATION_NAME = 'com.apple.BiometricKit.enrollmentChanged';
17
+ const BIOMETRIC_DOMAIN_COMPONENTS: Record<BiometricName, string> = {
18
+ touchId: 'fingerTouch',
19
+ faceId: 'pearl',
20
+ };
21
+
22
+ /**
23
+ * @param udid — UDID of the device to read from
24
+ * @returns whether a biometric sensor is currently simulated as enrolled
25
+ */
26
+ export async function isBiometricEnrolled(this: NativeSimctl, udid: string): Promise<boolean> {
27
+ return (await this.getDarwinNotificationState(udid, ENROLLMENT_NOTIFICATION_NAME)) === 1n;
28
+ }
29
+
30
+ /**
31
+ * Simulates enrolling (or un-enrolling) a biometric sensor — the prerequisite for
32
+ * {@link sendBiometricMatch} to have any effect.
33
+ *
34
+ * @param udid — UDID of the target device
35
+ * @param isEnabled — whether the device should report a biometric sensor as enrolled; defaults to `true`
36
+ */
37
+ export async function enrollBiometric(this: NativeSimctl, udid: string, isEnabled = true): Promise<void> {
38
+ await this.setDarwinNotificationState(udid, ENROLLMENT_NOTIFICATION_NAME, isEnabled ? 1n : 0n);
39
+ await this.postDarwinNotification(udid, ENROLLMENT_NOTIFICATION_NAME);
40
+ if ((await isBiometricEnrolled.call(this, udid)) !== isEnabled) {
41
+ throw new Error(`Failed to set biometric enrolled state for '${udid}' to '${isEnabled}'`);
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Simulates a successful or failed biometric match attempt — only takes effect once
47
+ * {@link enrollBiometric} has enrolled the corresponding sensor.
48
+ *
49
+ * @param udid — UDID of the target device
50
+ * @param shouldMatch — whether the simulated attempt should succeed; defaults to `true`
51
+ * @param biometricName — which sensor to simulate; defaults to `'touchId'`
52
+ */
53
+ export async function sendBiometricMatch(
54
+ this: NativeSimctl,
55
+ udid: string,
56
+ shouldMatch = true,
57
+ biometricName: BiometricName = 'touchId',
58
+ ): Promise<void> {
59
+ if (!Object.hasOwn(BIOMETRIC_DOMAIN_COMPONENTS, biometricName)) {
60
+ throw new Error(
61
+ `'${biometricName}' is not a valid biometric — use one of: ${Object.keys(BIOMETRIC_DOMAIN_COMPONENTS).join(', ')}`,
62
+ );
63
+ }
64
+ const domainComponent = BIOMETRIC_DOMAIN_COMPONENTS[biometricName];
65
+ await this.postDarwinNotification(
66
+ udid,
67
+ `com.apple.BiometricKit_Sim.${domainComponent}.${shouldMatch ? '' : 'no'}match`,
68
+ );
69
+ }
@@ -0,0 +1,51 @@
1
+ import type {NativeSimctl} from '../native-simctl.js';
2
+ import {runCatchingAsync} from '../utils/index.js';
3
+
4
+ declare module '../native-simctl.js' {
5
+ interface NativeSimctl {
6
+ getDarwinNotificationState(udid: string, name: string): Promise<bigint>;
7
+ setDarwinNotificationState(udid: string, name: string, state: bigint): Promise<void>;
8
+ postDarwinNotification(udid: string, name: string): Promise<void>;
9
+ }
10
+ }
11
+
12
+ /**
13
+ * The "state" here is Darwin's low-level `notify(3)` per-name state value (`notify_get_state`) —
14
+ * a full 64-bit integer any process can attach to a notification name independently of posting
15
+ * it, so a reader can check the last-set value without having been listening at post time.
16
+ * Returned as `bigint`, not `number` — a JS `number` only has 53 bits of safe integer precision,
17
+ * not enough for an arbitrary 64-bit counter or bit field another process may have stored.
18
+ *
19
+ * @see https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/notify.3.html
20
+ * @param udid — UDID of the device to read from
21
+ * @param name — Darwin notification name
22
+ * @returns the last-set state value for that notification (`0n` if never set)
23
+ */
24
+ export async function getDarwinNotificationState(this: NativeSimctl, udid: string, name: string): Promise<bigint> {
25
+ return runCatchingAsync(async () => (await this._findDevice(udid)).darwinNotificationGetState(name));
26
+ }
27
+
28
+ /**
29
+ * Sets the state value of a Darwin notification on the given device, without posting it (see
30
+ * {@link getDarwinNotificationState} for what "state" means here — `notify(3)`'s `notify_set_state`).
31
+ *
32
+ * @param udid — UDID of the target device
33
+ * @param name — Darwin notification name
34
+ * @param state — state value to store; must fit in an unsigned 64-bit integer (`0n` to `2n**64n-1n`)
35
+ */
36
+ export async function setDarwinNotificationState(
37
+ this: NativeSimctl,
38
+ udid: string,
39
+ name: string,
40
+ state: bigint,
41
+ ): Promise<void> {
42
+ return runCatchingAsync(async () => (await this._findDevice(udid)).darwinNotificationSetState(name, state));
43
+ }
44
+
45
+ /**
46
+ * @param udid — UDID of the target device
47
+ * @param name — Darwin notification name to post
48
+ */
49
+ export async function postDarwinNotification(this: NativeSimctl, udid: string, name: string): Promise<void> {
50
+ return runCatchingAsync(async () => (await this._findDevice(udid)).postDarwinNotification(name));
51
+ }
@@ -0,0 +1,74 @@
1
+ import type {NativeSimctl} from '../native-simctl.js';
2
+ import type {PushNotificationPayload} from '../types.js';
3
+ import {runCatchingAsync} from '../utils/index.js';
4
+
5
+ declare module '../native-simctl.js' {
6
+ interface NativeSimctl {
7
+ getEnv(udid: string, name: string): Promise<string>;
8
+ openUrl(udid: string, url: string): Promise<void>;
9
+ setLocation(udid: string, latitude: number, longitude: number): Promise<void>;
10
+ clearLocation(udid: string): Promise<void>;
11
+ pushNotification(udid: string, bundleId: string, payload: PushNotificationPayload): Promise<void>;
12
+ }
13
+ }
14
+
15
+ /**
16
+ * @param udid — UDID of the (booted) device to read from
17
+ * @param name — environment variable name
18
+ * @returns the variable's value
19
+ */
20
+ export async function getEnv(this: NativeSimctl, udid: string, name: string): Promise<string> {
21
+ return runCatchingAsync(async () => (await this._findDevice(udid)).getenv(name));
22
+ }
23
+
24
+ /**
25
+ * Opens a URL on the given device; iOS resolves the app matching the URL's scheme.
26
+ *
27
+ * @param udid — UDID of the target device
28
+ * @param url — the URL to open, e.g. `https://appium.io`
29
+ */
30
+ export async function openUrl(this: NativeSimctl, udid: string, url: string): Promise<void> {
31
+ return runCatchingAsync(async () => (await this._findDevice(udid)).openUrl(url));
32
+ }
33
+
34
+ /**
35
+ * Sets the given device's simulated GPS location.
36
+ *
37
+ * @param udid — UDID of the target device
38
+ * @param latitude — location latitude
39
+ * @param longitude — location longitude
40
+ */
41
+ export async function setLocation(
42
+ this: NativeSimctl,
43
+ udid: string,
44
+ latitude: number,
45
+ longitude: number,
46
+ ): Promise<void> {
47
+ return runCatchingAsync(async () => (await this._findDevice(udid)).setLocation(latitude, longitude));
48
+ }
49
+
50
+ /**
51
+ * Stops simulating a GPS location previously set via {@link setLocation}, reverting the device to
52
+ * its default (no simulated location) behavior.
53
+ *
54
+ * @param udid — UDID of the target device
55
+ */
56
+ export async function clearLocation(this: NativeSimctl, udid: string): Promise<void> {
57
+ return runCatchingAsync(async () => (await this._findDevice(udid)).clearLocation());
58
+ }
59
+
60
+ /**
61
+ * Delivers a simulated push notification to the given device.
62
+ *
63
+ * @param udid — UDID of the target device
64
+ * @param bundleId — bundle identifier of the app to receive the notification
65
+ * @param payload — see {@link PushNotificationPayload}
66
+ */
67
+ export async function pushNotification(
68
+ this: NativeSimctl,
69
+ udid: string,
70
+ bundleId: string,
71
+ payload: PushNotificationPayload,
72
+ ): Promise<void> {
73
+ return runCatchingAsync(async () => (await this._findDevice(udid)).sendPushNotification(bundleId, payload));
74
+ }
@@ -0,0 +1,63 @@
1
+ import {randomUUID} from 'node:crypto';
2
+ import {rm, writeFile} from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ import type {NativeSimctl} from '../native-simctl.js';
7
+ import {runCatchingAsync} from '../utils/index.js';
8
+
9
+ declare module '../native-simctl.js' {
10
+ interface NativeSimctl {
11
+ addCertificate(udid: string, cert: string | Buffer): Promise<void>;
12
+ addRootCertificate(udid: string, cert: string | Buffer): Promise<void>;
13
+ resetKeychain(udid: string): Promise<void>;
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Adds a certificate to the given device's keychain (not trusted as root).
19
+ *
20
+ * @param udid — UDID of the target device
21
+ * @param cert — path to a `.cert`/`.pem` file on disk, or the raw certificate content
22
+ */
23
+ export async function addCertificate(this: NativeSimctl, udid: string, cert: string | Buffer): Promise<void> {
24
+ const {path: certPath, cleanup} = await resolveCertPath(cert);
25
+ try {
26
+ return await runCatchingAsync(async () => (await this._findDevice(udid)).addCertificate(certPath, false));
27
+ } finally {
28
+ await cleanup();
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Adds a certificate to the given device's Trusted Root Store.
34
+ *
35
+ * @param udid — UDID of the target device
36
+ * @param cert — path to a `.cert`/`.pem` file on disk, or the raw certificate content
37
+ */
38
+ export async function addRootCertificate(this: NativeSimctl, udid: string, cert: string | Buffer): Promise<void> {
39
+ const {path: certPath, cleanup} = await resolveCertPath(cert);
40
+ try {
41
+ return await runCatchingAsync(async () => (await this._findDevice(udid)).addCertificate(certPath, true));
42
+ } finally {
43
+ await cleanup();
44
+ }
45
+ }
46
+
47
+ /** @param udid — UDID of the device whose keychain should be reset */
48
+ export async function resetKeychain(this: NativeSimctl, udid: string): Promise<void> {
49
+ return runCatchingAsync(async () => (await this._findDevice(udid)).resetKeychain());
50
+ }
51
+
52
+ /**
53
+ * The native call only accepts a file path — a `Buffer` is written to a throwaway temp file first,
54
+ * cleaned up by the caller once done.
55
+ */
56
+ async function resolveCertPath(cert: string | Buffer): Promise<{path: string; cleanup: () => Promise<void>}> {
57
+ if (typeof cert === 'string') {
58
+ return {path: cert, cleanup: async () => {}};
59
+ }
60
+ const tmpPath = path.join(os.tmpdir(), `${randomUUID()}.pem`);
61
+ await writeFile(tmpPath, cert);
62
+ return {path: tmpPath, cleanup: () => rm(tmpPath, {force: true})};
63
+ }
@@ -0,0 +1,210 @@
1
+ import {waitForCondition} from 'asyncbox';
2
+
3
+ import type {NativeSimctl} from '../native-simctl.js';
4
+ import {SimDeviceState, type NativeDeviceHandle, type SimBootInfo, type SimDeviceInfo} from '../types.js';
5
+ import type {SimDeviceTypeInfo, SimRuntimeInfo} from '../types.js';
6
+ import {runCatchingAsync} from '../utils/index.js';
7
+
8
+ const DEFAULT_BOOT_TIMEOUT_MS = 240_000;
9
+
10
+ declare module '../native-simctl.js' {
11
+ interface NativeSimctl {
12
+ getDevices(): Promise<SimDeviceInfo[]>;
13
+ getSupportedDeviceTypes(): Promise<SimDeviceTypeInfo[]>;
14
+ getSupportedRuntimes(): Promise<SimRuntimeInfo[]>;
15
+ createDevice(name: string, deviceTypeIdentifier: string, runtimeIdentifier: string): Promise<SimDeviceInfo>;
16
+ deleteDevice(udid: string): Promise<void>;
17
+ bootDevice(udid: string, options?: Record<string, unknown>): Promise<void>;
18
+ getBootStatus(udid: string): Promise<SimBootInfo | null>;
19
+ waitForBoot(udid: string, options?: {timeoutMs?: number}): Promise<void>;
20
+ shutdownDevice(udid: string): Promise<void>;
21
+ shutdownAllDevices(): Promise<void>;
22
+ eraseDevice(udid: string): Promise<void>;
23
+ }
24
+ }
25
+
26
+ /** @returns every device in the default device set. */
27
+ export async function getDevices(this: NativeSimctl): Promise<SimDeviceInfo[]> {
28
+ return runCatchingAsync(async () => {
29
+ const deviceSet = await this._deviceSet();
30
+ return (await deviceSet.devices()).map(toDeviceInfo);
31
+ });
32
+ }
33
+
34
+ /** @returns every simulator device type this CoreSimulator install supports (e.g. "iPhone 15"). */
35
+ export async function getSupportedDeviceTypes(this: NativeSimctl): Promise<SimDeviceTypeInfo[]> {
36
+ return runCatchingAsync(async () => (await this._serviceContext()).supportedDeviceTypes());
37
+ }
38
+
39
+ /** @returns every simulator runtime this CoreSimulator install supports (e.g. iOS 17.4). */
40
+ export async function getSupportedRuntimes(this: NativeSimctl): Promise<SimRuntimeInfo[]> {
41
+ return runCatchingAsync(async () => (await this._serviceContext()).supportedRuntimes());
42
+ }
43
+
44
+ /**
45
+ * Creates a new device in the default device set. Settles into the `Shutdown` state — never
46
+ * observed as `Creating` (see CLAUDE.md).
47
+ *
48
+ * @param name — display name for the new device
49
+ * @param deviceTypeIdentifier — e.g. `com.apple.CoreSimulator.SimDeviceType.iPhone-15`
50
+ * @param runtimeIdentifier — e.g. `com.apple.CoreSimulator.SimRuntime.iOS-17-4`
51
+ * @returns the newly created device
52
+ */
53
+ export async function createDevice(
54
+ this: NativeSimctl,
55
+ name: string,
56
+ deviceTypeIdentifier: string,
57
+ runtimeIdentifier: string,
58
+ ): Promise<SimDeviceInfo> {
59
+ return runCatchingAsync(async () => {
60
+ const deviceSet = await this._deviceSet();
61
+ return toDeviceInfo(await deviceSet.createDevice(deviceTypeIdentifier, runtimeIdentifier, name));
62
+ });
63
+ }
64
+
65
+ /**
66
+ * Deletes the given device from the default device set. Returns before the underlying
67
+ * filesystem cleanup finishes — eventually consistent (see CLAUDE.md).
68
+ *
69
+ * @param udid — UDID of the device to delete
70
+ */
71
+ export async function deleteDevice(this: NativeSimctl, udid: string): Promise<void> {
72
+ return runCatchingAsync(async () => {
73
+ const [deviceSet, device] = await Promise.all([this._deviceSet(), this._findDevice(udid)]);
74
+ await deviceSet.deleteDevice(device);
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Boots the given device; resolves only once CoreSimulator's own async completion handler
80
+ * fires (see CLAUDE.md for a rare eventual-consistency caveat on the resulting `state`).
81
+ *
82
+ * @param udid — UDID of the device to boot
83
+ * @param options — passed through to `bootWithOptions:`/`bootAsyncWithOptions:...:`; the
84
+ * option-dictionary keys are currently unverified (see CLAUDE.md)
85
+ */
86
+ export async function bootDevice(
87
+ this: NativeSimctl,
88
+ udid: string,
89
+ options: Record<string, unknown> = {},
90
+ ): Promise<void> {
91
+ return runCatchingAsync(async () => (await this._findDevice(udid)).boot(options));
92
+ }
93
+
94
+ /**
95
+ * Reads the device's current boot-progress status — the same underlying signal `simctl bootstatus`
96
+ * itself monitors (see CLAUDE.md), distinct from and more granular than `SimDeviceState`.
97
+ *
98
+ * @param udid — UDID of the device to read from
99
+ * @returns `null` if the device has never been booted; otherwise its most recent {@link SimBootInfo}.
100
+ * Confirmed empirically to **not** reset after shutdown — a shut-down device that was booted
101
+ * before still reports its last boot's terminal status, so check `getDevices()`'s `state` too if
102
+ * you need to know whether the device is *currently* booted (see {@link waitForBoot}, which does).
103
+ */
104
+ export async function getBootStatus(this: NativeSimctl, udid: string): Promise<SimBootInfo | null> {
105
+ return runCatchingAsync(async () => (await this._findDevice(udid)).getBootStatus());
106
+ }
107
+
108
+ /**
109
+ * Waits for the given device's boot to fully settle — matching `simctl bootstatus`'s own notion of
110
+ * "done" (`SimBootInfo.isTerminal`), not just `SimDeviceState` reaching `Booted`, which happens
111
+ * *tens of seconds* earlier while data migration/system-app startup are still in progress (see
112
+ * CLAUDE.md). Returns immediately if the device is already fully booted (its very first status
113
+ * check already sees `isTerminal`).
114
+ *
115
+ * @param udid — UDID of the device to monitor
116
+ * @param options.timeoutMs — how long to wait before giving up (default 4 minutes, matching
117
+ * `simctl bootstatus`'s own default timeout)
118
+ * @throws if the device isn't currently `Booting` or `Booted` — there's nothing to monitor
119
+ * @throws if the device stops booting (e.g. is shut down) before it finishes
120
+ * @throws if `timeoutMs` elapses before boot settles
121
+ */
122
+ export async function waitForBoot(this: NativeSimctl, udid: string, options: {timeoutMs?: number} = {}): Promise<void> {
123
+ const {timeoutMs = DEFAULT_BOOT_TIMEOUT_MS} = options;
124
+ return runCatchingAsync(async () => {
125
+ const initialState = (await this._findDevice(udid)).state();
126
+ if (initialState !== SimDeviceState.Booting && initialState !== SimDeviceState.Booted) {
127
+ throw new Error(`Device '${udid}' is not booting or booted (state: ${initialState})`);
128
+ }
129
+ // Re-resolves the device handle on every check (never caches it across the wait), matching
130
+ // every other method here — a device deleted mid-wait then surfaces as a normal "not found"
131
+ // error instead of a stale native reference failing in some less obvious way.
132
+ await waitForCondition(
133
+ async () => {
134
+ const device = await this._findDevice(udid);
135
+ const stateBefore = device.state();
136
+ if (stateBefore !== SimDeviceState.Booting && stateBefore !== SimDeviceState.Booted) {
137
+ // getBootStatus()'s isTerminal is confirmed to never reset on shutdown, so it would
138
+ // still read true from a *previous* boot session here if the device stopped booting
139
+ // partway through this wait (e.g. shut down by another caller) — checking current state
140
+ // too prevents reporting that stale old boot as this wait having succeeded.
141
+ throw new Error(`Device '${udid}' stopped booting before it finished (state: ${stateBefore})`);
142
+ }
143
+ const bootInfo = await device.getBootStatus();
144
+ // getBootStatus() is itself an async native call — the device can stop booting while it's
145
+ // in flight, which would otherwise let a stale isTerminal from the check above pass this
146
+ // predicate. Rechecking afterward, and requiring Booted specifically (not just Booting):
147
+ // isTerminal only ever turns true once state has already reached Booted (see CLAUDE.md —
148
+ // SimDeviceState reaches Booted well before boot info settles), so Booting + isTerminal can
149
+ // only mean a stale status from a previous boot session, never real completion.
150
+ const stateAfter = device.state();
151
+ if (stateAfter !== SimDeviceState.Booting && stateAfter !== SimDeviceState.Booted) {
152
+ throw new Error(`Device '${udid}' stopped booting before it finished (state: ${stateAfter})`);
153
+ }
154
+ return stateAfter === SimDeviceState.Booted && bootInfo?.isTerminal === true;
155
+ },
156
+ {
157
+ waitMs: timeoutMs,
158
+ intervalMs: 500,
159
+ error: `Device '${udid}' did not finish booting within ${timeoutMs}ms`,
160
+ },
161
+ );
162
+ });
163
+ }
164
+
165
+ /**
166
+ * @param udid — UDID of the device to shut down
167
+ * @throws if the device is already `Shutdown` — this is not an idempotent no-op
168
+ */
169
+ export async function shutdownDevice(this: NativeSimctl, udid: string): Promise<void> {
170
+ return runCatchingAsync(async () => (await this._findDevice(udid)).shutdown());
171
+ }
172
+
173
+ /**
174
+ * Best-effort shutdown of every device in the default device set that isn't already `Shutdown` —
175
+ * the native equivalent of `xcrun simctl shutdown all`. A fan-out over {@link shutdownDevice}
176
+ * rather than `SimDeviceSet`'s own bulk method, whose completion-block signature couldn't be
177
+ * confirmed safely (see commit message). Per-device failures are swallowed.
178
+ */
179
+ export async function shutdownAllDevices(this: NativeSimctl): Promise<void> {
180
+ return runCatchingAsync(async () => {
181
+ const deviceSet = await this._deviceSet();
182
+ const devices = await deviceSet.devices();
183
+ await Promise.all(
184
+ devices
185
+ .filter((device) => device.state() !== SimDeviceState.Shutdown)
186
+ .map((device) => device.shutdown().catch(() => {})),
187
+ );
188
+ });
189
+ }
190
+
191
+ /**
192
+ * Resets the given device's content and settings. Requires it to already be `Shutdown` — call
193
+ * {@link shutdownDevice} first if it's booted (see CLAUDE.md).
194
+ *
195
+ * @param udid — UDID of the device to erase
196
+ * @throws if the device isn't currently `Shutdown`
197
+ */
198
+ export async function eraseDevice(this: NativeSimctl, udid: string): Promise<void> {
199
+ return runCatchingAsync(async () => (await this._findDevice(udid)).erase());
200
+ }
201
+
202
+ function toDeviceInfo(device: NativeDeviceHandle): SimDeviceInfo {
203
+ return {
204
+ udid: device.udid(),
205
+ name: device.name(),
206
+ state: device.state() as SimDeviceState,
207
+ deviceTypeIdentifier: device.deviceTypeIdentifier(),
208
+ runtimeIdentifier: device.runtimeIdentifier(),
209
+ };
210
+ }