UNPKG

appium-xcuitest-driver

Version:

Appium driver for iOS using XCUITest for backend

250 lines 11.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.mobileStartXctestScreenRecording = mobileStartXctestScreenRecording; exports.mobileGetXctestScreenRecordingInfo = mobileGetXctestScreenRecordingInfo; exports.mobileStopXctestScreenRecording = mobileStopXctestScreenRecording; const node_os_1 = __importDefault(require("node:os")); const node_path_1 = __importDefault(require("node:path")); const support_1 = require("appium/support"); const remote_xpc_1 = require("../device/remote-xpc"); const xctest_attachment_deletion_client_1 = require("../device/xctest-attachment-deletion-client"); const helpers_1 = require("./helpers"); const MOV_EXT = '.mov'; /** * On simulators XCTest stores screen recording attachments under * `InternalDaemon/<id>/Attachments/<uuid>` (legacy) or * `InternalDaemon/<id>/tmp/Attachments/<uuid>` (Xcode 26.5+). * Brace `{,tmp/}` matches both in a single glob. */ const SIMULATOR_XCTEST_RECORDING_ATTACHMENT_GLOB = '*/{,tmp/}Attachments/*'; /** Insecure feature when real-device XCTest recording is used without RemoteXPC attachment deletion. */ const XCTEST_SCREEN_RECORD_FEATURE = 'xctest_screen_record'; const DOMAIN_IDENTIFIER = 'com.apple.testmanagerd'; const DOMAIN_TYPE = 'appDataContainer'; const USERNAME = 'mobile'; /** Legacy layout and Xcode 26.5+ `tmp/Attachments` within testmanagerd's app data container. */ const REAL_DEVICE_XCTEST_ATTACHMENT_SUBDIRECTORIES = ['Attachments', 'tmp/Attachments']; class XcTestScreenRecordingRetriever { log; constructor(log) { this.log = log; } static nameMatchesUuid(name, uuid) { return name.toUpperCase() === uuid.toUpperCase(); } } class SimulatorXcTestScreenRecordingRetriever extends XcTestScreenRecordingRetriever { device; constructor(device, log) { super(log); this.device = device; } async retrieve(uuid) { const dataRoot = this.device.getDir(); // e.g. .../CoreSimulator/Devices/<udid>/data/Containers/Data/InternalDaemon/<daemon-id>/Attachments/<uuid> // or .../InternalDaemon/<daemon-id>/tmp/Attachments/<uuid> (Xcode 26.5+) const internalDaemonRoot = node_path_1.default.resolve(dataRoot, 'Containers', 'Data', 'InternalDaemon'); const attachmentPaths = await support_1.fs.glob(SIMULATOR_XCTEST_RECORDING_ATTACHMENT_GLOB, { cwd: internalDaemonRoot, absolute: true, }); const videoPath = attachmentPaths.find((fp) => XcTestScreenRecordingRetriever.nameMatchesUuid(node_path_1.default.basename(fp), uuid)); if (!videoPath) { throw new Error(`Unable to locate XCTest screen recording identified by '${uuid}' for the Simulator ${this.device.udid}`); } const { size } = await support_1.fs.stat(videoPath); this.log.debug(`Located the video at '${videoPath}' (${support_1.util.toReadableSizeString(size)})`); return videoPath; } } class RealDeviceXcTestScreenRecordingRetriever extends XcTestScreenRecordingRetriever { device; tmpDir; constructor(device, tmpDir, log) { super(log); this.device = device; this.tmpDir = tmpDir; } async retrieve(uuid) { const attachment = await this.findAttachment(uuid); if (!attachment) { throw new Error(`Unable to locate XCTest screen recording identified by '${uuid}' for the device ${this.device.udid}`); } const videoPath = node_path_1.default.join(this.tmpDir, `${uuid}${MOV_EXT}`); const { subdirectory, fileName } = attachment; await this.device.devicectl.pullFile(`${subdirectory}/${fileName}`, videoPath, { username: USERNAME, domainIdentifier: DOMAIN_IDENTIFIER, domainType: DOMAIN_TYPE, }); const { size } = await support_1.fs.stat(videoPath); this.log.debug(`Pulled the video to '${videoPath}' (${support_1.util.toReadableSizeString(size)})`); return videoPath; } async findAttachment(uuid) { for (const subdirectory of REAL_DEVICE_XCTEST_ATTACHMENT_SUBDIRECTORIES) { let fileNames; try { fileNames = await this.device.devicectl.listFiles(DOMAIN_TYPE, DOMAIN_IDENTIFIER, { username: USERNAME, subdirectory, }); } catch { continue; } const fileName = fileNames.find((name) => XcTestScreenRecordingRetriever.nameMatchesUuid(name, uuid)); if (fileName) { return { subdirectory, fileName }; } } return null; } } /** * Start a new screen recording via XCTest. * * On **real devices**, if **iOS 18+** and a new enough **appium-ios-remotexpc** (with * **XCTestAttachment**) are present, the attachment is removed after stop and the * `xctest_screen_record` insecure feature is **not** required. * If deletion cannot be performed (older iOS, package missing, or too old), you must enable * the `xctest_screen_record` insecure feature to start recording. * * If the recording is already running this API is a noop. * * @since Xcode 15/iOS 17 * @param fps - FPS value * @param codec - Video codec, where 0 is h264, 1 is HEVC * @returns The information about a newly created or a running the screen recording. * @throws {Error} If screen recording has failed to start. */ async function mobileStartXctestScreenRecording(fps, codec) { if (this.isRealDevice()) { const canDeleteAfterStop = (await this.remoteXPCFacade?.determineAvailability()) ?? false; if (!canDeleteAfterStop) { this.assertFeatureEnabled(XCTEST_SCREEN_RECORD_FEATURE); } } const opts = {}; if (Number.isInteger(codec)) { opts.codec = codec; } if (Number.isInteger(fps)) { opts.fps = fps; } const response = (await this.proxyCommand('/wda/video/start', 'POST', opts)); this.log.info(`Started a new screen recording: ${JSON.stringify(response)}`); return response; } /** * Retrieves information about the current running screen recording. * If no screen recording is running then `null` is returned. */ async function mobileGetXctestScreenRecordingInfo() { return (await this.proxyCommand('/wda/video', 'GET')); } /** * Stop screen recording previously started by mobileStartXctestScreenRecording API. * * An error is thrown if no screen recording is running. * * The resulting movie is returned as base-64 string or is uploaded to * a remote location if corresponding options have been provided. * * The resulting movie is automatically deleted from the host temp file FOR SIMULATORS ONLY. * On **real devices**, after a successful pull the driver removes the XCTest attachment via * **appium-ios-remotexpc** when the same conditions hold as for starting without * `xctest_screen_record` (iOS 18+, package present, **XCTestAttachment** export). Otherwise * device-side delete is skipped. That deletion runs even if Base64 encoding or remote upload * fails afterward (the original encode/upload error is still thrown); if both fail, delete errors * are logged as warnings so the encode/upload failure remains primary. * * @since Xcode 15/iOS 17 * @param remotePath - The path to the remote location, where the resulting video should be * uploaded. * The following protocols are supported: `http`, `https`, `ftp`. Null or empty * string value (the default setting) means the content of resulting file * should be encoded as Base64 and passed as the endpoint response value. An * exception will be thrown if the generated media file is too big to fit into * the available process memory. * @param user - The name of the user for the remote authentication. * Only works if `remotePath` is provided. * @param pass - The password for the remote authentication. * Only works if `remotePath` is provided. * @param headers - Additional headers mapping for multipart http(s) uploads * @param fileFieldName - The name of the form field where the file content BLOB should be stored for * http(s) uploads * @param formFields - Additional form fields for multipart http(s) uploads * @param method - The http multipart upload method name. * Only works if `remotePath` is provided. * @returns The resulting movie with base64-encoded content or empty string if uploaded remotely. * @throws {Error} If there was an error while retrieving the video * file or the file content cannot be uploaded to the remote location. */ async function mobileStopXctestScreenRecording(remotePath, user, pass, headers, fileFieldName, formFields, method = 'PUT') { const screenRecordingInfo = await this.mobileGetXctestScreenRecordingInfo(); if (!screenRecordingInfo) { throw new Error('There is no active screen recording. Did you start one beforehand?'); } this.log.debug(`Stopping the active screen recording: ${JSON.stringify(screenRecordingInfo)}`); await this.proxyCommand('/wda/video/stop', 'POST', {}); const videoPath = await createXcTestScreenRecordingRetriever(this).retrieve(screenRecordingInfo.uuid); const result = { ...screenRecordingInfo, payload: '', // Will be set below }; let encodeOrUploadError; let attachmentDeleteError; try { result.payload = await (0, helpers_1.encodeBase64OrUpload)(videoPath, remotePath, { user, pass, headers, fileFieldName, formFields, method, }); } catch (err) { encodeOrUploadError = err; } finally { await support_1.fs.rimraf(videoPath); if (this.remoteXPCFacade?.eligible) { try { const deletionClient = new xctest_attachment_deletion_client_1.XctestAttachmentDeletionClient(this.remoteXPCFacade); await deletionClient.deleteAttachmentsByUuid([screenRecordingInfo.uuid]); } catch (deleteErr) { if (encodeOrUploadError === undefined) { if (this.isFeatureEnabled(XCTEST_SCREEN_RECORD_FEATURE) && (0, remote_xpc_1.isTunnelAvailabilityError)(deleteErr)) { this.log.warn(`Could not delete XCTest attachment on device: ${(0, remote_xpc_1.formatTunnelAvailabilityMessage)(deleteErr)}`); } else { attachmentDeleteError = deleteErr; } } else { this.log.warn(`Could not delete XCTest attachment on device (encode/upload had already failed): ${deleteErr?.message ?? deleteErr}`); } } } } if (encodeOrUploadError !== undefined) { throw encodeOrUploadError; } if (attachmentDeleteError !== undefined) { throw attachmentDeleteError; } return result; } function createXcTestScreenRecordingRetriever(driver) { if (driver.isRealDevice()) { return new RealDeviceXcTestScreenRecordingRetriever(driver.device, driver.opts.tmpDir || node_os_1.default.tmpdir(), driver.log); } return new SimulatorXcTestScreenRecordingRetriever(driver.device, driver.log); } //# sourceMappingURL=xctest-record-screen.js.map