UNPKG

appium-mac2-driver

Version:

XCTest-based Appium driver for macOS apps automation

309 lines 13.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.NativeVideoChunksBroadcaster = void 0; exports.macosStartNativeScreenRecording = macosStartNativeScreenRecording; exports.macosGetNativeScreenRecordingInfo = macosGetNativeScreenRecordingInfo; exports.macosStopNativeScreenRecording = macosStopNativeScreenRecording; exports.macosListDisplays = macosListDisplays; const lodash_1 = __importDefault(require("lodash")); const bluebird_1 = __importStar(require("bluebird")); const node_path_1 = __importDefault(require("node:path")); const support_1 = require("appium/support"); const helpers_1 = require("./helpers"); const asyncbox_1 = require("asyncbox"); const teen_process_1 = require("teen_process"); const constants_1 = require("./bidi/constants"); const models_1 = require("./bidi/models"); const RECORDING_STARTUP_TIMEOUT_MS = 5000; const BUFFER_SIZE = 0xffff; const MONITORING_INTERVAL_DURATION_MS = 1000; const MAX_MONITORING_DURATION_MS = 24 * 60 * 60 * 1000; // 1 day class NativeVideoChunksBroadcaster { _ee; _log; _publishers; _terminated; constructor(ee, log) { this._ee = ee; this._log = log; this._publishers = new Map(); this._terminated = false; } get hasPublishers() { return this._publishers.size > 0; } schedule(uuid) { if (!this._publishers.has(uuid)) { this._publishers.set(uuid, this._createPublisher(uuid)); } } async waitFor(uuid) { const publisher = this._publishers.get(uuid); if (publisher) { await publisher; } } async shutdown(timeoutMs) { try { await this._wait(timeoutMs); } catch (e) { this._log.warn(e.message); } await this._cleanup(); this._publishers = new Map(); } async _createPublisher(uuid) { let fullPath = ''; let bytesRead = 0n; try { await (0, asyncbox_1.waitForCondition)(async () => { const paths = await listAttachments(); const result = paths.find((name) => name.endsWith(uuid)); if (result) { fullPath = result; return true; } return false; }, { waitMs: RECORDING_STARTUP_TIMEOUT_MS, intervalMs: 300, }); } catch { throw new Error(`The video recording identified by ${uuid} did not ` + `start within ${RECORDING_STARTUP_TIMEOUT_MS}ms timeout`); } const startedMs = performance.now(); while (!this._terminated && performance.now() - startedMs < MAX_MONITORING_DURATION_MS) { const isCompleted = !(await isFileUsed(fullPath, 'testman')); const { size } = await support_1.fs.stat(fullPath, { bigint: true }); if (bytesRead < size) { const handle = await support_1.fs.open(fullPath, 'r'); try { while (bytesRead < size) { const bufferSize = Number(size - bytesRead > BUFFER_SIZE ? BUFFER_SIZE : size - bytesRead); const buf = Buffer.alloc(bufferSize); await support_1.fs.read(handle, buf, 0, bufferSize, bytesRead); this._ee.emit(constants_1.BIDI_EVENT_NAME, (0, models_1.toNativeVideoChunkAddedEvent)(uuid, buf)); bytesRead += BigInt(bufferSize); } } finally { await support_1.fs.close(handle); } } if (isCompleted) { this._log.debug(`The native video recording identified by ${uuid} has been detected as completed`); return; } await bluebird_1.default.delay(MONITORING_INTERVAL_DURATION_MS); } this._log.warn(`Stopped monitoring of the native video recording identified by ${uuid} ` + `because of the timeout`); } async _wait(timeoutMs) { if (!this.hasPublishers) { return; } const timer = setTimeout(() => { this._terminated = true; }, timeoutMs); const publishingErrors = []; for (const publisher of this._publishers.values()) { try { await publisher; } catch (e) { publishingErrors.push(e.message); } } clearTimeout(timer); if (!lodash_1.default.isEmpty(publishingErrors)) { throw new Error(publishingErrors.join('\n')); } } async _cleanup() { if (!this.hasPublishers) { return; } const attachments = await listAttachments(); if (lodash_1.default.isEmpty(attachments)) { return; } const tasks = attachments .map((attachmentPath) => [node_path_1.default.basename(attachmentPath), attachmentPath]) .filter(([name]) => this._publishers.has(name)) .map(([, attachmentPath]) => support_1.fs.rimraf(attachmentPath)); if (lodash_1.default.isEmpty(tasks)) { return; } try { await Promise.all(tasks); this._log.debug(`Successfully deleted ${support_1.util.pluralize('leftover video recording', tasks.length, true)}`); } catch (e) { this._log.warn(`Could not cleanup some leftover video recordings: ${e.message}`); } } } exports.NativeVideoChunksBroadcaster = NativeVideoChunksBroadcaster; /** * Initiates a new native screen recording session via XCTest. * If the screen recording is already running then this call results in noop. * A screen recording is running until a testing session is finished. * If a recording has never been stopped explicitly during a test session * then it would be stopped automatically upon test session termination, * and leftover videos would be deleted as well. * * @since Xcode 15 * @param fps Frame Per Second setting for the resulting screen recording. 24 by default. * @param codec Possible codec value, where `0` means H264 (the default setting), `1` means HEVC * @param displayId Valid display identifier to record the video from. Main display is assumed * by default. * @returns The information about the asynchronously running video recording. */ async function macosStartNativeScreenRecording(fps, codec, displayId) { const result = (await this.wda.proxy.command('/wda/video/start', 'POST', { fps, codec, displayId, })); this._videoChunksBroadcaster.schedule(result.uuid); return result; } /** * @since Xcode 15 * @returns The information about the asynchronously running video recording or * null if no native video recording has been started. */ async function macosGetNativeScreenRecordingInfo() { return (await this.wda.proxy.command('/wda/video', 'GET')); } /** * Stops native screen recordind. * If no screen recording has been started before then the method throws an exception. * * @since Xcode 15 * @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. * @param pass The password for the remote authentication. * @param method The http multipart upload method name. The 'PUT' one is used by default. * @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 ignorePayload Whether to ignore the resulting video payload * and return an empty string. Useful if you prefer to fetch * video chunks via a BiDi web socket. * @returns Base64-encoded content of the recorded media file if 'remotePath' * parameter is falsy or an empty string or ignorePayload is set to `true`. * @throws {Error} If there was an error while getting the name of a media file * or the file content cannot be uploaded to the remote location * or screen recording is not supported on the device under test. */ async function macosStopNativeScreenRecording(remotePath, user, pass, method, headers, fileFieldName, formFields, ignorePayload) { const response = (await this.wda.proxy.command('/wda/video/stop', 'POST', {})); if (!response || !lodash_1.default.isPlainObject(response)) { throw new Error('There is no active screen recording, thus nothing to stop. Did you start it before?'); } const { uuid } = response; try { await bluebird_1.default.resolve(this._videoChunksBroadcaster.waitFor(uuid)).timeout(5000); } catch (e) { if (e instanceof bluebird_1.TimeoutError) { this.log.debug(`The BiDi chunks broadcaster for the native screen recording identified ` + `by ${uuid} cannot complete within 5000ms timeout`); } else { this.log.debug(e.stack); } } if (ignorePayload) { return ''; } const matchedVideoPath = lodash_1.default.first((await listAttachments()).filter((name) => name.endsWith(uuid))); if (!matchedVideoPath) { throw new Error(`The screen recording identified by ${uuid} cannot be retrieved. ` + `Make sure the Appium Server process or its parent process (e.g. Terminal) ` + `has Full Disk Access permission enabled in 'System Preferences' -> 'Privacy & Security' tab. ` + `You may verify the presence of the recorded video manually by running the ` + `'find "$HOME/Library/Daemon Containers/" -type f -name "${uuid}"' command from Terminal ` + `if the latter has been granted the above access permission.`); } const options = { user, pass, method, headers, fileFieldName, formFields, }; return await helpers_1.uploadRecordedMedia.bind(this)(matchedVideoPath, remotePath, options); } /** * Fetches information about available displays * * @returns A map where keys are display identifiers and values are display infos */ async function macosListDisplays() { return (await this.wda.proxy.command('/wda/displays/list', 'GET')); } // #region Private functions async function listAttachments() { // The expected path looks like // $HOME/Library/Daemon Containers/EFDD24BF-F856-411F-8954-CD5F0D6E6F3E/Data/Attachments/CAE7E5E2-5AC9-4D33-A47B-C491D644DE06 const deamonContainersRoot = node_path_1.default.resolve(process.env.HOME, 'Library', 'Daemon Containers'); return await support_1.fs.glob(`*/Data/Attachments/*`, { cwd: deamonContainersRoot, absolute: true, }); } async function isFileUsed(fpath, userProcessName) { const { stdout } = await (0, teen_process_1.exec)('lsof', [fpath]); return stdout.includes(userProcessName); } // #endregion //# sourceMappingURL=native-record-screen.js.map