UNPKG

appium-xcuitest-driver

Version:

Appium driver for iOS using XCUITest for backend

176 lines (153 loc) 5.4 kB
import {logger} from 'appium/support'; import {errors} from 'appium/driver'; import type {StringRecord} from '@appium/types'; import type {XCTestEvent, XCTestRunnerOptions, XCTestRunStage} from 'appium-ios-remotexpc'; import type {XCTestResult, RunXCTestResult} from '../commands/types'; import {getXCTestRunnerClass} from './remotexpc-utils'; import {InstallationProxyClient} from './installation-proxy-client'; const xctestLog = logger.getLogger('XCTest:RemoteXPC'); /** * Run an XCTest suite via RemoteXPC using the high-level XCTestRunner. * * The XCTestRunner handles the full lifecycle internally: * 1. Service discovery (testmanagerd, DVT, InstallationProxy) via single RemoteXPC connection * 2. App lookup to resolve test bundle paths * 3. Exec/control session initialization * 4. Test runner process launch via ProcessControl * 5. XCTestConfiguration delivery and test plan execution * 6. Event-driven completion with typed callbacks * 7. Cleanup (kill process, close connections) * * Per-test results are collected via XCTestRunner's 'xctest' events. */ export async function runXCTestViaRemoteXPC( udid: string, testRunnerBundleId: string, appUnderTestBundleId: string, xctestBundleId: string, testType: 'app' | 'ui' | 'logic' = 'ui', args: string[] = [], env?: StringRecord, timeout = 360000, ): Promise<RunXCTestResult> { // Logic tests don't use testmanagerd — skip RemoteXPC if (testType === 'logic') { throw new Error('Logic tests are not supported via RemoteXPC'); } const XCTestRunnerClass = await getXCTestRunnerClass(); const runnerOptions: XCTestRunnerOptions = { udid, testRunnerBundleId, appUnderTestBundleId, xctestBundleId, timeoutMs: timeout, launchEnvironment: env as Record<string, string>, launchArguments: args, killExisting: true, testType, // Already narrowed to 'ui' | 'app' after the logic-test guard above }; const runner = new XCTestRunnerClass(runnerOptions); // Collect per-test results from typed events const results: XCTestResult[] = []; const pendingFailures = new Map<string, {message: string; location: string}>(); runner.on('xctest', (event: XCTestEvent) => { switch (event.type) { case 'testCaseFailed': { const identifier = `${event.testClass}/${event.method}`; pendingFailures.set(identifier, { message: event.message, location: `${event.file}:${event.line}`, }); break; } case 'testCaseFinished': { const passed = event.status === 'passed'; const crashed = event.status === 'crashed'; const result: XCTestResult = { testName: event.identifier, passed, crashed, status: event.status || 'failed', duration: event.duration, }; // Attach pending failure info if any const failure = pendingFailures.get(event.identifier); if (failure) { result.failureMessage = failure.message; result.location = failure.location; pendingFailures.delete(event.identifier); } results.push(result); break; } default: break; } }); runner.on('step', (stage: XCTestRunStage) => { xctestLog.info(`XCTest step: ${stage}`); }); const runResult = await runner.run(); if (runResult.error) { xctestLog.warn(`XCTest run completed with error: ${runResult.error}`); } if (runResult.status === 'timed_out') { throw new errors.TimeoutError( `Timed out after '${timeout}ms' waiting for XCTest to complete via RemoteXPC`, ); } const allPassed = results.length > 0 ? results.every((r) => r.passed) : (runResult.testSummary?.failureCount ?? 0) === 0; return { results, code: allPassed ? 0 : 1, signal: null, passed: allPassed, }; } /** * List XCTest bundles installed on the device via RemoteXPC. * Uses InstallationProxy to browse apps and filter for xctrunner bundles. */ export async function listXCTestBundlesViaRemoteXPC(udid: string): Promise<string[]> { const installProxy = await InstallationProxyClient.create(udid, true); try { const apps = await installProxy.listApplications({ applicationType: 'User', returnAttributes: ['CFBundleIdentifier', 'CFBundleExecutable', 'Path'], }); const bundles: string[] = []; for (const [bundleId, info] of Object.entries(apps)) { // Match xctrunner bundles or apps whose path contains .xctest if (bundleId.endsWith('.xctrunner') || (info.Path && String(info.Path).includes('.xctest'))) { bundles.push(bundleId); } } return bundles; } finally { await installProxy.close(); } } /** * Install an XCTest bundle via RemoteXPC. * Only supports .ipa and .app bundles (not bare .xctest directories). */ export async function installXCTestBundleViaRemoteXPC( udid: string, xctestApp: string, ): Promise<void> { if (xctestApp.endsWith('.xctest')) { throw new Error( 'Bare .xctest bundles cannot be installed via RemoteXPC. Provide a .app or .ipa instead.', ); } const installProxy = await InstallationProxyClient.create(udid, true); try { await installProxy.installApplication(xctestApp); xctestLog.info(`Installed XCTest bundle: ${xctestApp}`); } finally { await installProxy.close(); } }