UNPKG

appium-xcuitest-driver

Version:

Appium driver for iOS using XCUITest for backend

159 lines (148 loc) 5.22 kB
import type {AppiumLogger} from '@appium/types'; import {getRemoteXPCServices} from './remotexpc-utils'; import type { NotificationProxyService as RemoteXPCNotificationProxyService, RemoteXpcConnection, } from 'appium-ios-remotexpc'; import {services} from 'appium-ios-device'; import type {NotificationProxy as IOSDeviceNotificationProxy} from 'appium-ios-device'; /** * Unified Notification Proxy Client * * Provides a unified interface for notification proxy operations on iOS devices, * automatically handling the differences between iOS < 18 (appium-ios-device) * and iOS 18 and above (appium-ios-remotexpc NotificationProxyService). */ export class NotificationClient { private readonly service: RemoteXPCNotificationProxyService | IOSDeviceNotificationProxy; private readonly remoteXPCConnection?: RemoteXpcConnection; private readonly log: AppiumLogger; private constructor( service: RemoteXPCNotificationProxyService | IOSDeviceNotificationProxy, log: AppiumLogger, remoteXPCConnection?: RemoteXpcConnection, ) { this.service = service; this.log = log; this.remoteXPCConnection = remoteXPCConnection; } /** * Check if this client is using RemoteXPC */ private get isRemoteXPC(): boolean { return !!this.remoteXPCConnection; } /** * Get service as RemoteXPC NotificationProxyService */ private get remoteXPCNotificationProxy(): RemoteXPCNotificationProxyService { return this.service as RemoteXPCNotificationProxyService; } /** * Get service as iOS Device NotificationProxy */ private get iosDeviceNotificationProxy(): IOSDeviceNotificationProxy { return this.service as IOSDeviceNotificationProxy; } /** * Create a notification client for device * * @param udid - Device UDID * @param log - Appium logger instance * @param useRemoteXPC - Whether to use remotexpc (use isIos18OrNewer(opts) to determine) * @returns NotificationClient instance */ static async create( udid: string, log: AppiumLogger, useRemoteXPC: boolean, ): Promise<NotificationClient> { if (useRemoteXPC) { const client = await NotificationClient.withRemoteXpcConnection(async () => { const Services = await getRemoteXPCServices(); const {notificationProxyService, remoteXPC} = await Services.startNotificationProxyService(udid); return { service: notificationProxyService, connection: remoteXPC, }; }, log); if (client) { return client; } } // Fallback to appium-ios-device const notificationProxy = await services.startNotificationProxyService(udid); return new NotificationClient(notificationProxy, log); } /** * Helper to safely execute remoteXPC operations with connection cleanup * @param operation - Async operation that returns service and connection * @param log - Logger instance * @returns NotificationClient on success, null on failure */ private static async withRemoteXpcConnection< T extends RemoteXPCNotificationProxyService | IOSDeviceNotificationProxy, >( operation: () => Promise<{service: T; connection: RemoteXpcConnection}>, log: AppiumLogger, ): Promise<NotificationClient | null> { let remoteXPCConnection: RemoteXpcConnection | undefined; let succeeded = false; try { const {service, connection} = await operation(); remoteXPCConnection = connection; const client = new NotificationClient(service, log, remoteXPCConnection); succeeded = true; return client; } catch (err: any) { log.error( `Failed to create notification client via RemoteXPC: ${err.message}, falling back to appium-ios-device`, ); return null; } finally { // Only close connection if we failed (if succeeded, the client owns it) if (remoteXPCConnection && !succeeded) { try { await remoteXPCConnection.close(); } catch { // Ignore cleanup errors } } } } /** * Observe a specific notification and wait for it * * @param notificationName - Name of the notification to observe * @returns Promise that resolves when the notification is received */ async observeNotification(notificationName: string): Promise<void> { if (this.isRemoteXPC) { await this.remoteXPCNotificationProxy.observe(notificationName); } else { // iOS Device: Use callback-based observation wrapped in a promise return new Promise((resolve) => { this.iosDeviceNotificationProxy.observeNotification(notificationName, { notification: resolve, }); }); } } /** * Close the notification service connection and remoteXPC connection if present */ async close(): Promise<void> { // Close the service first this.service.close(); // Then close RemoteXPC connection if present if (this.remoteXPCConnection) { try { this.log.debug(`Closing remoteXPC connection`); await this.remoteXPCConnection.close(); } catch (err: any) { this.log.debug(`Error closing remoteXPC connection: ${err.message}`); } } } }