appium-xcuitest-driver 10.15.0 → 10.16.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.
@@ -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';
@@ -122,18 +122,23 @@ export async function mobileInstallCertificate(
122
122
  if (!this.opts.udid) {
123
123
  throw new Error('udid capability is required');
124
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')});
131
- return;
132
- } else {
133
- this.log.info(
134
- 'pyidevice is not installed on your system. ' +
135
- 'Falling back to the (slow) UI-based installation',
125
+
126
+ let client: CertificateClient | null = null;
127
+ try {
128
+ client = await CertificateClient.create(
129
+ this.opts.udid,
130
+ this.log,
131
+ isIos18OrNewer(this.opts)
136
132
  );
133
+ await client.installCertificate({payload: Buffer.from(content, 'base64')});
134
+ return;
135
+ } catch (err) {
136
+ this.log.error(`Failed to install the certificate: ${err.message}`);
137
+ this.log.info('Falling back to the (slow) UI-based installation');
138
+ } finally {
139
+ if (client) {
140
+ await client.close();
141
+ }
137
142
  }
138
143
  }
139
144
 
@@ -256,12 +261,20 @@ export async function mobileRemoveCertificate(this: XCUITestDriver, name: string
256
261
  if (!this.opts.udid) {
257
262
  throw new Error('udid capability is required');
258
263
  }
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);
264
+
265
+ let client: CertificateClient | null = null;
266
+ try {
267
+ client = await CertificateClient.create(
268
+ this.opts.udid,
269
+ this.log,
270
+ isIos18OrNewer(this.opts)
271
+ );
272
+ return await client.removeCertificate(name);
273
+ } finally {
274
+ if (client) {
275
+ await client.close();
276
+ }
277
+ }
265
278
  }
266
279
 
267
280
  /**
@@ -278,14 +291,21 @@ export async function mobileListCertificates(this: XCUITestDriver): Promise<Cert
278
291
  if (!this.opts.udid) {
279
292
  throw new Error('udid capability is required');
280
293
  }
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();
287
- }
288
294
 
295
+ let client: CertificateClient | null = null;
296
+ try {
297
+ client = await CertificateClient.create(
298
+ this.opts.udid,
299
+ this.log,
300
+ isIos18OrNewer(this.opts)
301
+ );
302
+ return await client.listCertificates();
303
+ } finally {
304
+ if (client) {
305
+ await client.close();
306
+ }
307
+ }
308
+ }
289
309
 
290
310
  /**
291
311
  * Extracts the common name of the certificate from the given buffer.
@@ -501,4 +521,3 @@ async function installPost122Certificate(driver: XCUITestDriver, name: string):
501
521
 
502
522
  return true;
503
523
  }
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.0",
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.0",
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.0",
12
12
  "author": "Appium Contributors",
13
13
  "license": "Apache-2.0",
14
14
  "repository": {