@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.
- package/LICENSE +201 -0
- package/README.md +66 -0
- package/binding.gyp +53 -0
- package/lib/src/commands/app.d.ts +88 -0
- package/lib/src/commands/app.d.ts.map +1 -0
- package/lib/src/commands/app.js +133 -0
- package/lib/src/commands/app.js.map +1 -0
- package/lib/src/commands/biometric.d.ts +33 -0
- package/lib/src/commands/biometric.d.ts.map +1 -0
- package/lib/src/commands/biometric.js +44 -0
- package/lib/src/commands/biometric.js.map +1 -0
- package/lib/src/commands/darwin-notification.d.ts +36 -0
- package/lib/src/commands/darwin-notification.d.ts.map +1 -0
- package/lib/src/commands/darwin-notification.js +35 -0
- package/lib/src/commands/darwin-notification.js.map +1 -0
- package/lib/src/commands/interaction.d.ts +48 -0
- package/lib/src/commands/interaction.d.ts.map +1 -0
- package/lib/src/commands/interaction.js +48 -0
- package/lib/src/commands/interaction.js.map +1 -0
- package/lib/src/commands/keychain.d.ts +25 -0
- package/lib/src/commands/keychain.d.ts.map +1 -0
- package/lib/src/commands/keychain.js +52 -0
- package/lib/src/commands/keychain.js.map +1 -0
- package/lib/src/commands/lifecycle.d.ts +101 -0
- package/lib/src/commands/lifecycle.d.ts.map +1 -0
- package/lib/src/commands/lifecycle.js +165 -0
- package/lib/src/commands/lifecycle.js.map +1 -0
- package/lib/src/commands/media.d.ts +28 -0
- package/lib/src/commands/media.d.ts.map +1 -0
- package/lib/src/commands/media.js +27 -0
- package/lib/src/commands/media.js.map +1 -0
- package/lib/src/commands/misc.d.ts +15 -0
- package/lib/src/commands/misc.d.ts.map +1 -0
- package/lib/src/commands/misc.js +12 -0
- package/lib/src/commands/misc.js.map +1 -0
- package/lib/src/commands/pasteboard.d.ts +24 -0
- package/lib/src/commands/pasteboard.d.ts.map +1 -0
- package/lib/src/commands/pasteboard.js +22 -0
- package/lib/src/commands/pasteboard.js.map +1 -0
- package/lib/src/commands/permissions.d.ts +48 -0
- package/lib/src/commands/permissions.d.ts.map +1 -0
- package/lib/src/commands/permissions.js +84 -0
- package/lib/src/commands/permissions.js.map +1 -0
- package/lib/src/commands/process.d.ts +16 -0
- package/lib/src/commands/process.d.ts.map +1 -0
- package/lib/src/commands/process.js +68 -0
- package/lib/src/commands/process.js.map +1 -0
- package/lib/src/commands/screenshot.d.ts +29 -0
- package/lib/src/commands/screenshot.d.ts.map +1 -0
- package/lib/src/commands/screenshot.js +30 -0
- package/lib/src/commands/screenshot.js.map +1 -0
- package/lib/src/commands/spawn.d.ts +48 -0
- package/lib/src/commands/spawn.d.ts.map +1 -0
- package/lib/src/commands/spawn.js +108 -0
- package/lib/src/commands/spawn.js.map +1 -0
- package/lib/src/commands/ui.d.ts +42 -0
- package/lib/src/commands/ui.d.ts.map +1 -0
- package/lib/src/commands/ui.js +44 -0
- package/lib/src/commands/ui.js.map +1 -0
- package/lib/src/commands/webinspector.d.ts +19 -0
- package/lib/src/commands/webinspector.d.ts.map +1 -0
- package/lib/src/commands/webinspector.js +16 -0
- package/lib/src/commands/webinspector.js.map +1 -0
- package/lib/src/errors.d.ts +51 -0
- package/lib/src/errors.d.ts.map +1 -0
- package/lib/src/errors.js +82 -0
- package/lib/src/errors.js.map +1 -0
- package/lib/src/index.d.ts +7 -0
- package/lib/src/index.d.ts.map +1 -0
- package/lib/src/index.js +5 -0
- package/lib/src/index.js.map +1 -0
- package/lib/src/native-simctl.d.ts +62 -0
- package/lib/src/native-simctl.d.ts.map +1 -0
- package/lib/src/native-simctl.js +221 -0
- package/lib/src/native-simctl.js.map +1 -0
- package/lib/src/types.d.ts +267 -0
- package/lib/src/types.d.ts.map +1 -0
- package/lib/src/types.js +29 -0
- package/lib/src/types.js.map +1 -0
- package/lib/src/utils/index.d.ts +3 -0
- package/lib/src/utils/index.d.ts.map +1 -0
- package/lib/src/utils/index.js +3 -0
- package/lib/src/utils/index.js.map +1 -0
- package/lib/src/utils/pkg-root.d.ts +6 -0
- package/lib/src/utils/pkg-root.d.ts.map +1 -0
- package/lib/src/utils/pkg-root.js +12 -0
- package/lib/src/utils/pkg-root.js.map +1 -0
- package/lib/src/utils/run-catching.d.ts +3 -0
- package/lib/src/utils/run-catching.d.ts.map +1 -0
- package/lib/src/utils/run-catching.js +11 -0
- package/lib/src/utils/run-catching.js.map +1 -0
- package/package.json +73 -0
- package/scripts/install.mjs +22 -0
- package/src/commands/app.ts +186 -0
- package/src/commands/biometric.ts +69 -0
- package/src/commands/darwin-notification.ts +51 -0
- package/src/commands/interaction.ts +74 -0
- package/src/commands/keychain.ts +63 -0
- package/src/commands/lifecycle.ts +210 -0
- package/src/commands/media.ts +38 -0
- package/src/commands/misc.ts +20 -0
- package/src/commands/pasteboard.ts +31 -0
- package/src/commands/permissions.ts +122 -0
- package/src/commands/process.ts +80 -0
- package/src/commands/screenshot.ts +46 -0
- package/src/commands/spawn.ts +133 -0
- package/src/commands/ui.ts +61 -0
- package/src/commands/webinspector.ts +23 -0
- package/src/coresim.mm +1066 -0
- package/src/errors.ts +93 -0
- package/src/index.ts +23 -0
- package/src/native/async_bridge.h +164 -0
- package/src/native/nserror_bridge.h +64 -0
- package/src/native/nserror_bridge.mm +30 -0
- package/src/native/objc_runtime.h +45 -0
- package/src/native/objc_runtime.mm +76 -0
- package/src/native/safe_dispatch.h +38 -0
- package/src/native/sim_device.h +143 -0
- package/src/native/sim_device.mm +355 -0
- package/src/native/sim_device_set.h +16 -0
- package/src/native/sim_device_set.mm +40 -0
- package/src/native/sim_pasteboard.h +16 -0
- package/src/native/sim_pasteboard.mm +295 -0
- package/src/native/sim_process.h +13 -0
- package/src/native/sim_process.mm +139 -0
- package/src/native/sim_screenshot.h +28 -0
- package/src/native/sim_screenshot.mm +226 -0
- package/src/native/sim_service_context.h +42 -0
- package/src/native/sim_service_context.mm +92 -0
- package/src/native/tcc_privacy.h +35 -0
- package/src/native/tcc_privacy.mm +256 -0
- package/src/native/value_bridge.h +18 -0
- package/src/native/value_bridge.mm +111 -0
- package/src/native-simctl.ts +288 -0
- package/src/types.ts +302 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/pkg-root.ts +14 -0
- 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
|
+
}
|