appium-xcuitest-driver
Version:
Appium driver for iOS using XCUITest for backend
211 lines • 10.3 kB
JavaScript
;
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 support_1 = require("appium/support");
const helpers_1 = require("./helpers");
const utils_1 = require("../utils");
const node_path_1 = __importDefault(require("node:path"));
const xctest_attachment_deletion_client_1 = require("../device/xctest-attachment-deletion-client");
const remotexpc_utils_1 = require("../device/remotexpc-utils");
const MOV_EXT = '.mov';
/** 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';
const SUBDIRECTORY = 'Attachments';
/**
* 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 xctest_attachment_deletion_client_1.XctestAttachmentDeletionClient.isDeletionAvailable(this.opts.udid ?? '', this.opts.platformVersion ?? '', undefined, this.log);
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 retrieveXcTestScreenRecording.call(this, 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.isRealDevice() && this.opts.udid) {
try {
const canDelete = await xctest_attachment_deletion_client_1.XctestAttachmentDeletionClient.isDeletionAvailable(this.opts.udid, this.opts.platformVersion ?? '', undefined, this.log);
if (canDelete) {
const deletionClient = await xctest_attachment_deletion_client_1.XctestAttachmentDeletionClient.create(this.opts.udid, this.opts.platformVersion ?? '');
await deletionClient.deleteAttachmentsByUuid([screenRecordingInfo.uuid]);
}
else {
this.log.debug('Skipping XCTest attachment deletion on device (RemoteXPC deletion not available for this session)');
}
}
catch (deleteErr) {
if (encodeOrUploadError === undefined) {
if (this.isFeatureEnabled(XCTEST_SCREEN_RECORD_FEATURE) &&
(0, remotexpc_utils_1.isTunnelAvailabilityError)(deleteErr)) {
this.log.warn(`Could not delete XCTest attachment on device: ${(0, remotexpc_utils_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;
}
async function retrieveRecodingFromSimulator(uuid) {
const device = this.device;
const dataRoot = device.getDir();
// On Simulators the path looks like
// $HOME/Library/Developer/CoreSimulator/Devices/F8E1968A-8443-4A9A-AB86-27C54C36A2F6/data/Containers/Data/InternalDaemon/4E3FE8DF-AD0A-41DA-B6EC-C35E5798C219/Attachments/A044DAF7-4A58-4CD5-95C3-29B4FE80C377
const internalDaemonRoot = node_path_1.default.resolve(dataRoot, 'Containers', 'Data', 'InternalDaemon');
const videoPaths = await support_1.fs.glob(`*/Attachments/${uuid}`, {
cwd: internalDaemonRoot,
absolute: true,
});
if ((0, utils_1.isEmpty)(videoPaths)) {
throw new Error(`Unable to locate XCTest screen recording identified by '${uuid}' for the Simulator ${device.udid}`);
}
const videoPath = videoPaths[0];
const { size } = await support_1.fs.stat(videoPath);
this.log.debug(`Located the video at '${videoPath}' (${support_1.util.toReadableSizeString(size)})`);
return videoPath;
}
async function retrieveRecodingFromRealDevice(uuid) {
const device = this.device;
const fileNames = await device.devicectl.listFiles(DOMAIN_TYPE, DOMAIN_IDENTIFIER, {
username: USERNAME,
subdirectory: SUBDIRECTORY,
});
if (!fileNames.includes(uuid)) {
throw new Error(`Unable to locate XCTest screen recording identified by '${uuid}' for the device ${this.opts.udid}`);
}
if (!this.opts.tmpDir) {
throw new Error('tmpDir is not set in driver options');
}
const videoPath = node_path_1.default.join(this.opts.tmpDir, `${uuid}${MOV_EXT}`);
await device.devicectl.pullFile(`${SUBDIRECTORY}/${uuid}`, 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 function retrieveXcTestScreenRecording(uuid) {
return this.isRealDevice()
? await retrieveRecodingFromRealDevice.call(this, uuid)
: await retrieveRecodingFromSimulator.call(this, uuid);
}
//# sourceMappingURL=xctest-record-screen.js.map