appium-xcuitest-driver
Version:
Appium driver for iOS using XCUITest for backend
242 lines (213 loc) • 8.74 kB
text/typescript
import type {AppiumLogger} from '@appium/types';
import {getSimulator, type Simulator} from 'appium-ios-simulator';
import {getAndCheckIosSdkVersion} from '../commands/helpers';
import {UDID_AUTO} from '../constants';
import type {XCUITestDriverOpts} from '../driver';
import {normalizePlatformVersion} from '../utils';
import {getConnectedDevices, RealDevice} from './real-device-management';
import {isStrictHostUtilityMode} from './wda-host-ops';
export interface DeviceDiscoveryResult {
device: Simulator | RealDevice;
realDevice: boolean;
udid: string;
createdSimulator: boolean;
iosSdkVersion: string | null;
platformVersion?: string;
}
export interface DeviceDiscoveryOptions {
driverOpts: XCUITestDriverOpts;
log: AppiumLogger;
detectUdid: () => Promise<string>;
getExistingSimulator: (opts: XCUITestDriverOpts) => Promise<Simulator | null | undefined>;
createSimulator: (opts: XCUITestDriverOpts) => Promise<Simulator>;
}
interface DeviceDiscoveryStrategy {
isApplicable: (sessionOpts: XCUITestDriverOpts) => boolean;
determine: () => Promise<DeviceDiscoveryResult>;
}
export class DeviceDiscovery {
private createdSimulator = false;
private iosSdkVersion: string | null = null;
private platformVersion: string | undefined;
constructor(private readonly config: DeviceDiscoveryOptions) {}
private get log(): AppiumLogger {
return this.config.log;
}
private get sessionOpts(): XCUITestDriverOpts {
return this.config.driverOpts;
}
private get resolvedSessionOpts(): XCUITestDriverOpts {
return {
...this.sessionOpts,
platformVersion: this.platformVersion,
};
}
async determine(): Promise<DeviceDiscoveryResult> {
// in the one case where we create a sim, we will set this state
this.createdSimulator = false;
this.iosSdkVersion = null;
this.platformVersion = this.sessionOpts.platformVersion;
const isStrictHostMode = isStrictHostUtilityMode(this.sessionOpts);
return await this.selectStrategy(isStrictHostMode).determine();
}
private selectStrategy(isStrictHostMode: boolean): DeviceDiscoveryStrategy {
const strategies: DeviceDiscoveryStrategy[] = [
{
isApplicable: ({udid}) => udid?.toLowerCase() === UDID_AUTO,
determine: async () => await this.determineDeviceWithAutoUdid(isStrictHostMode),
},
{
isApplicable: ({udid}) => Boolean(udid),
determine: async () => await this.determineDeviceWithExplicitUdid(isStrictHostMode),
},
{
isApplicable: () => true,
determine: async () => await this.determineSimulatorDevice(isStrictHostMode),
},
];
return strategies.find((strategy) => strategy.isApplicable(this.sessionOpts)) as DeviceDiscoveryStrategy;
}
private async setupSimulatorPlatformVersion(): Promise<void> {
const iosSdkVersion = await getAndCheckIosSdkVersion();
this.iosSdkVersion = iosSdkVersion;
this.log.info(`iOS SDK Version set to '${iosSdkVersion}'`);
if (!this.sessionOpts.platformVersion && iosSdkVersion) {
this.log.info(
`No platformVersion specified. Using the latest version Xcode supports: '${iosSdkVersion}'. ` +
`This may cause problems if a simulator does not exist for this platform version.`,
);
this.platformVersion = normalizePlatformVersion(iosSdkVersion);
}
}
private async ensurePlatformVersion(device: Simulator | RealDevice): Promise<void> {
if (this.platformVersion) {
return;
}
this.platformVersion = await device.getPlatformVersion();
this.log.info(`No platformVersion specified. Using device version: '${this.platformVersion}'`);
}
private async createRealDeviceForUdid(udid: string): Promise<DeviceDiscoveryResult> {
this.log.debug(`Creating iDevice object with udid '${udid}'`);
const device = new RealDevice(udid, this.sessionOpts, this.log);
await this.ensurePlatformVersion(device);
return this.toResult({device, realDevice: true, udid});
}
private async determineDeviceWithAutoUdid(isStrictHostMode: boolean): Promise<DeviceDiscoveryResult> {
if (isStrictHostMode) {
throw new Error(
`Automatic device selection requires macOS device discovery utilities. ` +
`Set an explicit real-device 'appium:udid' when running from '${process.platform}'.`,
);
}
try {
const udid = await this.config.detectUdid();
return await this.createRealDeviceForUdid(udid);
} catch (err) {
this.log.warn(
`Cannot detect any connected real devices. Falling back to Simulator. Original error: ${
(err as Error).message
}`,
);
await this.setupSimulatorPlatformVersion();
const device = await this.config.getExistingSimulator(this.resolvedSessionOpts);
if (!device) {
throw this.log.errorWithException(
`Cannot detect udid for ${this.sessionOpts.deviceName} Simulator running iOS ${this.platformVersion}`,
);
}
await this.ensurePlatformVersion(device);
return this.toResult({device, realDevice: false, udid: device.udid});
}
}
private async determineDeviceWithExplicitUdid(isStrictHostMode: boolean): Promise<DeviceDiscoveryResult> {
const udid = this.sessionOpts.udid as string;
let isRealDeviceUdid = false;
// If webDriverAgentUrl is set with a real device, assume the user prepared the device.
const shouldCheckAvailableRealDevices = !this.sessionOpts.webDriverAgentUrl;
if (shouldCheckAvailableRealDevices) {
if (isStrictHostMode) {
isRealDeviceUdid = true;
} else {
const devices = await getConnectedDevices(this.sessionOpts);
this.log.debug(`Available real devices: ${devices.join(', ')}`);
isRealDeviceUdid = devices.includes(udid);
}
}
if (!isRealDeviceUdid) {
const simulatorDevice = await this.determineSimulatorWithExplicitUdid(
udid,
shouldCheckAvailableRealDevices,
isStrictHostMode,
);
if (simulatorDevice) {
return simulatorDevice;
}
}
return await this.createRealDeviceForUdid(udid);
}
private async determineSimulatorWithExplicitUdid(
udid: string,
shouldCheckAvailableRealDevices: boolean,
isStrictHostMode: boolean,
): Promise<DeviceDiscoveryResult | null> {
if (isStrictHostMode) {
this.log.debug(
`Skipping Simulator lookup for '${udid}' because the selected session ` +
`strategy must not use macOS Simulator utilities`,
);
return null;
}
try {
const device = await getSimulator(udid, {
devicesSetPath: this.sessionOpts.simulatorDevicesSetPath,
logger: this.log,
});
await this.ensurePlatformVersion(device);
return this.toResult({device, realDevice: false, udid});
} catch {
if (shouldCheckAvailableRealDevices) {
throw new Error(`Unknown device or simulator UDID: '${udid}'`);
}
this.log.debug(
'Skipping checking of the real devices availability since the session specifies appium:webDriverAgentUrl',
);
return null;
}
}
private async determineSimulatorDevice(isStrictHostMode: boolean): Promise<DeviceDiscoveryResult> {
this.log.info(
`No real device udid has been provided in capabilities. ` + `Will select a matching simulator to run the test.`,
);
if (isStrictHostMode) {
throw new Error(
`A real-device 'appium:udid' is required when running from '${process.platform}' without ` +
`macOS Simulator utilities.`,
);
}
await this.setupSimulatorPlatformVersion();
if (this.sessionOpts.enforceFreshSimulatorCreation) {
this.log.debug(
`New simulator is requested. If this is not wanted, set 'enforceFreshSimulatorCreation' capability to false`,
);
} else {
const device = await this.config.getExistingSimulator(this.resolvedSessionOpts);
if (device) {
await this.ensurePlatformVersion(device);
return this.toResult({device, realDevice: false, udid: device.udid});
}
}
this.log.info('Using desired caps to create a new simulator');
this.createdSimulator = true;
const device = await this.config.createSimulator(this.resolvedSessionOpts);
await this.ensurePlatformVersion(device);
return this.toResult({device, realDevice: false, udid: device.udid});
}
private toResult(result: Pick<DeviceDiscoveryResult, 'device' | 'realDevice' | 'udid'>): DeviceDiscoveryResult {
return {
...result,
createdSimulator: this.createdSimulator,
iosSdkVersion: this.iosSdkVersion,
platformVersion: this.platformVersion,
};
}
}