appium-xcuitest-driver 10.15.0 → 10.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,8 +7,8 @@ import path from 'path';
7
7
  import http from 'http';
8
8
  import {exec} from 'teen_process';
9
9
  import {findAPortNotInUse, checkPortStatus} from 'portscanner';
10
- import {Pyidevice} from '../device/clients/py-ios-device-client';
11
- import {requireRealDevice} from '../utils';
10
+ import {CertificateClient} from '../device/certificate-client';
11
+ import {requireRealDevice, isIos18OrNewer} from '../utils';
12
12
  import type {Simulator} from 'appium-ios-simulator';
13
13
  import type {XCUITestDriver} from '../driver';
14
14
  import type {CertificateList} from './types';
@@ -119,21 +119,14 @@ export async function mobileInstallCertificate(
119
119
  );
120
120
  }
121
121
  } else {
122
- if (!this.opts.udid) {
123
- throw new Error('udid capability is required');
124
- }
125
- const client = new Pyidevice({
126
- udid: this.opts.udid,
127
- log: this.log,
128
- });
129
- if (await client.assertExists(false)) {
130
- await client.installProfile({payload: Buffer.from(content, 'base64')});
122
+ try {
123
+ await withCertificateClient(this, async (client) => {
124
+ await client.installCertificate({payload: Buffer.from(content, 'base64')});
125
+ });
131
126
  return;
132
- } else {
133
- this.log.info(
134
- 'pyidevice is not installed on your system. ' +
135
- 'Falling back to the (slow) UI-based installation',
136
- );
127
+ } catch (err) {
128
+ this.log.error(`Failed to install the certificate: ${err.message}`);
129
+ this.log.info('Falling back to the (slow) UI-based installation');
137
130
  }
138
131
  }
139
132
 
@@ -161,7 +154,7 @@ export async function mobileInstallCertificate(
161
154
  try {
162
155
  const host = os.hostname();
163
156
  const certUrl = `http://${host}:${tmpPort}/${configName}`;
164
- await tmpServer.listen(tmpPort);
157
+ tmpServer.listen(tmpPort);
165
158
  try {
166
159
  await waitForCondition(
167
160
  async () => {
@@ -233,7 +226,7 @@ export async function mobileInstallCertificate(
233
226
 
234
227
  return (await util.toInMemoryBase64(configPath)).toString();
235
228
  } finally {
236
- await tmpServer.close();
229
+ tmpServer.close();
237
230
  await fs.rimraf(tmpRoot);
238
231
  }
239
232
  }
@@ -253,15 +246,7 @@ export async function mobileInstallCertificate(
253
246
  */
254
247
  export async function mobileRemoveCertificate(this: XCUITestDriver, name: string): Promise<string> {
255
248
  requireRealDevice(this, 'Removing certificate');
256
- if (!this.opts.udid) {
257
- throw new Error('udid capability is required');
258
- }
259
- const client = new Pyidevice({
260
- udid: this.opts.udid,
261
- log: this.log,
262
- });
263
- await client.assertExists(true);
264
- return await client.removeProfile(name);
249
+ return await withCertificateClient(this, async (client) => client.removeCertificate(name));
265
250
  }
266
251
 
267
252
  /**
@@ -275,17 +260,37 @@ export async function mobileRemoveCertificate(this: XCUITestDriver, name: string
275
260
  */
276
261
  export async function mobileListCertificates(this: XCUITestDriver): Promise<CertificateList> {
277
262
  requireRealDevice(this, 'Listing certificates');
278
- if (!this.opts.udid) {
279
- throw new Error('udid capability is required');
280
- }
281
- const client = new Pyidevice({
282
- udid: this.opts.udid,
283
- log: this.log,
284
- });
285
- await client.assertExists(true);
286
- return await client.listProfiles();
263
+ return await withCertificateClient(this, async (client) => client.listCertificates());
287
264
  }
288
265
 
266
+ /**
267
+ * Helper function to create a CertificateClient, execute an operation, and ensure cleanup.
268
+ *
269
+ * @param driver - The XCUITestDriver instance
270
+ * @param operation - A callback function that receives the client and performs the operation
271
+ * @returns The result of the operation callback
272
+ */
273
+ async function withCertificateClient<T>(
274
+ driver: XCUITestDriver,
275
+ operation: (client: CertificateClient) => Promise<T>,
276
+ ): Promise<T> {
277
+ let client: CertificateClient | null = null;
278
+ try {
279
+ if (!driver.opts.udid) {
280
+ throw new Error('udid capability is required');
281
+ }
282
+ client = await CertificateClient.create(
283
+ driver.opts.udid,
284
+ driver.log,
285
+ isIos18OrNewer(driver.opts),
286
+ );
287
+ return await operation(client);
288
+ } finally {
289
+ if (client) {
290
+ await client.close();
291
+ }
292
+ }
293
+ }
289
294
 
290
295
  /**
291
296
  * Extracts the common name of the certificate from the given buffer.
@@ -501,4 +506,3 @@ async function installPost122Certificate(driver: XCUITestDriver, name: string):
501
506
 
502
507
  return true;
503
508
  }
504
-
@@ -0,0 +1,195 @@
1
+ import type {AppiumLogger} from '@appium/types';
2
+ import {Pyidevice} from './clients/py-ios-device-client';
3
+ import {getRemoteXPCServices} from './remotexpc-utils';
4
+ import type {CertificateList} from '../commands/types';
5
+ import type {
6
+ MobileConfigService as RemoteXPCMobileConfigService,
7
+ RemoteXpcConnection,
8
+ } from 'appium-ios-remotexpc';
9
+
10
+ /**
11
+ * Options for installing a certificate
12
+ */
13
+ export interface InstallCertificateOptions {
14
+ payload: Buffer;
15
+ }
16
+
17
+ /**
18
+ * Unified Certificate Client
19
+ *
20
+ * Provides a unified interface for certificate operations on iOS devices,
21
+ * automatically handling the differences between iOS < 18 (py-ios-device)
22
+ * and iOS 18 and above (appium-ios-remotexpc MobileConfigService).
23
+ */
24
+ export class CertificateClient {
25
+ private readonly service: RemoteXPCMobileConfigService | Pyidevice;
26
+ private readonly remoteXPCConnection?: RemoteXpcConnection;
27
+ private readonly log: AppiumLogger;
28
+
29
+ private constructor(
30
+ service: RemoteXPCMobileConfigService | Pyidevice,
31
+ log: AppiumLogger,
32
+ remoteXPCConnection?: RemoteXpcConnection
33
+ ) {
34
+ this.service = service;
35
+ this.log = log;
36
+ this.remoteXPCConnection = remoteXPCConnection;
37
+ }
38
+
39
+ //#region Public Methods
40
+
41
+ /**
42
+ * Create a certificate client for device
43
+ *
44
+ * @param udid - Device UDID
45
+ * @param log - Appium logger instance
46
+ * @param useRemoteXPC - Whether to use remotexpc (use isIos18OrNewer(opts) to determine)
47
+ * @returns Certificate client instance
48
+ */
49
+ static async create(
50
+ udid: string,
51
+ log: AppiumLogger,
52
+ useRemoteXPC: boolean
53
+ ): Promise<CertificateClient> {
54
+ if (useRemoteXPC) {
55
+ const client = await CertificateClient.withRemoteXpcConnection(async () => {
56
+ const Services = await getRemoteXPCServices();
57
+ const {mobileConfigService, remoteXPC} = await Services.startMobileConfigService(udid);
58
+ return {
59
+ service: mobileConfigService,
60
+ connection: remoteXPC,
61
+ };
62
+ }, log);
63
+ if (client) {
64
+ return client;
65
+ }
66
+ }
67
+
68
+ // Fallback to py-ios-device
69
+ const pyideviceClient = new Pyidevice({
70
+ udid,
71
+ log,
72
+ });
73
+ await pyideviceClient.assertExists(true);
74
+ return new CertificateClient(pyideviceClient, log);
75
+ }
76
+
77
+ /**
78
+ * Install a certificate profile
79
+ *
80
+ * @param options - Installation options containing the certificate payload
81
+ */
82
+ async installCertificate(options: InstallCertificateOptions): Promise<void> {
83
+ const {payload} = options;
84
+
85
+ if (this.isRemoteXPC) {
86
+ await this.mobileConfigService.installProfileFromBuffer(payload);
87
+ } else {
88
+ await this.pyideviceClient.installProfile({payload});
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Remove a certificate profile by name
94
+ *
95
+ * @param name - Name of the profile to remove
96
+ * @returns Status message ('Acknowledged' for RemoteXPC, or pyidevice output)
97
+ */
98
+ async removeCertificate(name: string): Promise<string> {
99
+ if (this.isRemoteXPC) {
100
+ await this.mobileConfigService.removeProfile(name);
101
+ return 'Acknowledged';
102
+ } else {
103
+ return await this.pyideviceClient.removeProfile(name);
104
+ }
105
+ }
106
+
107
+ /**
108
+ * List installed certificate profiles
109
+ *
110
+ * @returns List of installed certificates
111
+ */
112
+ async listCertificates(): Promise<CertificateList> {
113
+ if (this.isRemoteXPC) {
114
+ return await this.mobileConfigService.getProfileList();
115
+ } else {
116
+ return await this.pyideviceClient.listProfiles();
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Close the certificate service connection and remoteXPC connection if present
122
+ */
123
+ async close(): Promise<void> {
124
+ if (this.remoteXPCConnection) {
125
+ try {
126
+ this.log.debug(`Closing remoteXPC connection`);
127
+ await this.remoteXPCConnection.close();
128
+ } catch (err: any) {
129
+ this.log.debug(`Error closing remoteXPC connection: ${err.message}`);
130
+ }
131
+ }
132
+ }
133
+
134
+ //#endregion
135
+
136
+ //#region Private Methods
137
+
138
+ /**
139
+ * Check if this client is using RemoteXPC
140
+ */
141
+ private get isRemoteXPC(): boolean {
142
+ return !!this.remoteXPCConnection;
143
+ }
144
+
145
+ /**
146
+ * Helper to safely execute remoteXPC operations with connection cleanup
147
+ * @param operation - Async operation that returns service and connection
148
+ * @param log - Logger instance
149
+ * @returns CertificateClient on success, null on failure
150
+ */
151
+ private static async withRemoteXpcConnection<T extends RemoteXPCMobileConfigService | Pyidevice>(
152
+ operation: () => Promise<{service: T; connection: RemoteXpcConnection}>,
153
+ log: AppiumLogger
154
+ ): Promise<CertificateClient | null> {
155
+ let remoteXPCConnection: RemoteXpcConnection | undefined;
156
+ let succeeded = false;
157
+ try {
158
+ const {service, connection} = await operation();
159
+ remoteXPCConnection = connection;
160
+ const client = new CertificateClient(service, log, remoteXPCConnection);
161
+ succeeded = true;
162
+ return client;
163
+ } catch (err: any) {
164
+ log.error(
165
+ `Failed to create certificate client via RemoteXPC: ${err.message}, falling back to py-ios-device`
166
+ );
167
+ return null;
168
+ } finally {
169
+ // Only close connection if we failed (if succeeded, the client owns it)
170
+ if (remoteXPCConnection && !succeeded) {
171
+ try {
172
+ await remoteXPCConnection.close();
173
+ } catch {
174
+ // Ignore cleanup errors
175
+ }
176
+ }
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Get service as RemoteXPC MobileConfigService
182
+ */
183
+ private get mobileConfigService(): RemoteXPCMobileConfigService {
184
+ return this.service as RemoteXPCMobileConfigService;
185
+ }
186
+
187
+ /**
188
+ * Get service as Pyidevice client
189
+ */
190
+ private get pyideviceClient(): Pyidevice {
191
+ return this.service as Pyidevice;
192
+ }
193
+
194
+ //#endregion
195
+ }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "appium-xcuitest-driver",
3
- "version": "10.15.0",
3
+ "version": "10.16.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "appium-xcuitest-driver",
9
- "version": "10.15.0",
9
+ "version": "10.16.1",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@appium/strongbox": "^1.0.0-rc.1",
@@ -692,9 +692,9 @@
692
692
  }
693
693
  },
694
694
  "node_modules/appium-ios-remotexpc": {
695
- "version": "0.25.2",
696
- "resolved": "https://registry.npmjs.org/appium-ios-remotexpc/-/appium-ios-remotexpc-0.25.2.tgz",
697
- "integrity": "sha512-mRMVjorxf70Q8qsDxoF8O5tDQ5N0hBYELQ6dUDnIUNI8YljcT1/SzlBnGLLfgZYuTK6ImyJhw/jifMJKjHEkrw==",
695
+ "version": "0.26.0",
696
+ "resolved": "https://registry.npmjs.org/appium-ios-remotexpc/-/appium-ios-remotexpc-0.26.0.tgz",
697
+ "integrity": "sha512-9c7z05u2vlwhn6OPyY60Tfug3U5LPNw1k6Se0qTRUxnGs7ttWltk+5Dn+QUy4XDRKCtogmv/azVWHBlfIz2xKg==",
698
698
  "license": "Apache-2.0",
699
699
  "optional": true,
700
700
  "dependencies": {
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "xcuitest",
9
9
  "xctest"
10
10
  ],
11
- "version": "10.15.0",
11
+ "version": "10.16.1",
12
12
  "author": "Appium Contributors",
13
13
  "license": "Apache-2.0",
14
14
  "repository": {