appium-xcuitest-driver
Version:
Appium driver for iOS using XCUITest for backend
230 lines (207 loc) • 7.47 kB
text/typescript
import type {Simulator} from 'appium-ios-simulator';
import type {
RealDevicePreinstalledHostOps,
SimulatorHostOps,
WdaHostOps,
WdaLaunchEnvironment,
} from 'appium-webdriveragent';
import type {XCUITestDriver} from '../driver';
import {isIos18OrNewerPlatform} from '../utils';
import type {RealDevice} from './real-device-management';
const XCODE_ONLY_CAPS = [
'usePrebuiltWDA',
'useXctestrunFile',
'prebuildWDA',
'xcodeOrgId',
'xcodeSigningId',
'xcodeConfigFile',
'keychainPath',
'keychainPassword',
'allowProvisioningDeviceRegistration',
'resultBundlePath',
] as const;
interface HostStrategyCaps {
udid?: string;
platformVersion?: string;
webDriverAgentUrl?: string;
usePreinstalledWDA?: boolean;
[key: string]: any;
}
/**
* Whether the selected session strategy must avoid host-side Xcode/Simulator utilities.
*/
export function isStrictHostUtilityMode(opts: HostStrategyCaps, platform: NodeJS.Platform = process.platform): boolean {
return platform !== 'darwin' && Boolean(opts.webDriverAgentUrl || opts.usePreinstalledWDA);
}
/**
* Verifies host-only utility requirements that can be checked before device discovery.
*/
export function assertWdaHostSessionCapsSupported(
opts: HostStrategyCaps,
platform: NodeJS.Platform = process.platform,
): void {
if (platform === 'darwin') {
return;
}
if (!opts.webDriverAgentUrl && !opts.usePreinstalledWDA) {
throw new Error(
`The selected XCUITest session strategy requires macOS with Xcode. ` +
`Use 'appium:usePreinstalledWDA' with a signed prebuilt WebDriverAgent or provide ` +
`'appium:webDriverAgentUrl' to run from '${platform}'.`,
);
}
if (!opts.udid || opts.udid.toLowerCase() === 'auto') {
throw new Error(
`Running XCUITest from '${platform}' without macOS/Xcode requires an explicit real-device ` +
`'appium:udid'. Simulator discovery and automatic device selection require macOS.`,
);
}
if (opts.usePreinstalledWDA && !opts.webDriverAgentUrl && !opts.platformVersion) {
throw new Error(
`Running preinstalled WebDriverAgent from '${platform}' requires 'appium:platformVersion' ` +
`so RemoteXPC eligibility can be checked without probing host Xcode or Simulator tools.`,
);
}
const xcodeOnlyCaps = XCODE_ONLY_CAPS.filter((capName) => Boolean(opts[capName]));
if (opts.usePreinstalledWDA && !opts.webDriverAgentUrl && xcodeOnlyCaps.length > 0) {
throw new Error(
`The following capabilities require macOS/Xcode and cannot be used with the ` +
`RemoteXPC preinstalled WebDriverAgent strategy on '${platform}': ` +
xcodeOnlyCaps.join(', '),
);
}
}
/**
* Creates WebDriverAgent host operations provided by the XCUITest driver.
*/
export function createWdaHostOps(driver: XCUITestDriver): WdaHostOps {
return {
simulator: createSimulatorHostOps(driver),
realDevicePreinstalled: createRealDevicePreinstalledHostOps(driver),
};
}
/**
* Verifies the selected WDA lifecycle can run on the current host platform.
*/
export function assertWdaHostPlatformSupported(driver: XCUITestDriver): void {
if (process.platform === 'darwin') {
return;
}
assertWdaHostSessionCapsSupported(driver.opts);
if (driver.opts.webDriverAgentUrl) {
return;
}
if (!driver.isRealDevice()) {
throw new Error(
`XCUITest simulator sessions require macOS with Xcode. ` +
`Use a real device with 'appium:usePreinstalledWDA' or provide 'appium:webDriverAgentUrl' ` +
`to run from '${process.platform}'.`,
);
}
if (!driver.opts.usePreinstalledWDA) {
throw new Error(
`The default real-device WebDriverAgent startup strategy requires macOS with Xcode. ` +
`Use 'appium:usePreinstalledWDA' with a signed prebuilt WebDriverAgent or provide ` +
`'appium:webDriverAgentUrl' to run from '${process.platform}'.`,
);
}
if (driver.opts.platformVersion && !isIos18OrNewerPlatform(driver.opts.platformVersion)) {
throw new Error(
`Running preinstalled WebDriverAgent from '${process.platform}' requires a real device ` +
`with RemoteXPC tunnel support. The current platformVersion is ` +
`'${driver.opts.platformVersion}'; use iOS/tvOS 18.0 or newer, or provide ` +
`'appium:webDriverAgentUrl' for an externally managed WDA.`,
);
}
if (!driver.remoteXPCFacade.eligible) {
throw new Error(
`RemoteXPC is required to launch preinstalled WebDriverAgent from '${process.platform}', ` +
`but this session is not eligible for RemoteXPC.`,
);
}
}
function stringifyLaunchEnvironment(env: WdaLaunchEnvironment): Record<string, string> {
return Object.entries(env).reduce<Record<string, string>>((acc, [key, value]) => {
acc[key] = String(value);
return acc;
}, {});
}
function createSimulatorHostOps(driver: XCUITestDriver): SimulatorHostOps {
return {
async launchPreinstalled({udid, bundleId, env}) {
await (driver.device as Simulator).simctl.exec('launch', {
args: ['--terminate-running-process', udid, bundleId],
env,
});
},
async terminate({bundleId}) {
await (driver.device as Simulator).terminateApp(bundleId);
},
};
}
function createRealDevicePreinstalledHostOps(driver: XCUITestDriver): RealDevicePreinstalledHostOps {
return {
async launchPreinstalled({udid, bundleId, env}) {
try {
const dvt = await driver.remoteXPCFacade.requireService('launch preinstalled WebDriverAgent', (Services) =>
Services.startDVTService(udid),
);
try {
await dvt.processControl.launch({
bundleId,
environment: stringifyLaunchEnvironment(env),
killExisting: true,
});
} finally {
await dvt.dvtService.close();
}
} catch (err) {
if (process.platform !== 'darwin') {
throw err;
}
driver.log.warn(
`Failed to launch preinstalled WebDriverAgent via RemoteXPC. ` +
`Falling back to devicectl. Original error: ${(err as Error).message}`,
);
const {devicectl} = driver.device as RealDevice;
if (!devicectl) {
throw err;
}
await devicectl.launchApp(bundleId, {
env,
terminateExisting: true,
});
}
},
async terminate({udid, bundleId}) {
try {
const dvt = await driver.remoteXPCFacade.requireService('terminate preinstalled WebDriverAgent', (Services) =>
Services.startDVTService(udid),
);
try {
const pid = await dvt.processControl.getPidForBundleIdentifier(bundleId);
if (!pid) {
driver.log.info(`The preinstalled WebDriverAgent process '${bundleId}' was not running`);
return;
}
await dvt.processControl.kill(pid);
} finally {
await dvt.dvtService.close();
}
} catch (err) {
if (process.platform !== 'darwin') {
throw err;
}
driver.log.warn(
`Failed to terminate preinstalled WebDriverAgent via RemoteXPC. ` +
`Falling back to devicectl. Original error: ${(err as Error).message}`,
);
const {devicectl} = driver.device as RealDevice;
if (!devicectl) {
throw err;
}
await devicectl.terminateApp(bundleId);
}
},
};
}