UNPKG

appium-safari-driver

Version:
315 lines 12.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.startRecordingScreen = startRecordingScreen; exports.stopRecordingScreen = stopRecordingScreen; const lodash_1 = __importDefault(require("lodash")); const support_1 = require("appium/support"); const asyncbox_1 = require("asyncbox"); const node_simctl_1 = require("node-simctl"); const STARTUP_INTERVAL_MS = 300; const STARTUP_TIMEOUT_MS = 10 * 1000; const DEFAULT_TIME_LIMIT_MS = 60 * 10 * 1000; // 10 minutes const PROCESS_SHUTDOWN_TIMEOUT_MS = 10 * 1000; const DEFAULT_EXT = '.mp4'; /** * * @param {string} localFile * @param {string|null} remotePath * @param {Object} uploadOptions * @returns */ async function uploadRecordedMedia(localFile, remotePath = null, uploadOptions = {}) { if (lodash_1.default.isEmpty(remotePath) || !remotePath) { return (await support_1.util.toInMemoryBase64(localFile)).toString(); } const { user, pass, method, headers, fileFieldName, formFields } = uploadOptions; const options = { method: method || 'PUT', headers, fileFieldName, formFields, }; if (user && pass) { options.auth = { user, pass }; } await support_1.net.uploadFile(localFile, remotePath, options); return ''; } const VIDEO_FILES = new Set(); process.on('exit', () => { for (const videoFile of VIDEO_FILES) { try { support_1.fs.rimrafSync(videoFile); } catch { } } }); class ScreenRecorder { /** * @param {string} udid * @param {string} videoPath * @param {import('@appium/types').AppiumLogger} log * @param {{codec?: string, display?: string, mask?: string, timeLimit?: string|number}} [opts={}] */ constructor(udid, videoPath, log, opts = {}) { this.log = log; this._process = null; this._udid = udid; this._videoPath = videoPath; this._codec = opts.codec; this._display = opts.display; this._mask = opts.mask; this._timeLimitMs = DEFAULT_TIME_LIMIT_MS; if (opts.timeLimit) { const timeLimitMs = parseInt(String(opts.timeLimit), 10); if (timeLimitMs > 0) { this._timeLimitMs = timeLimitMs * 1000; } } this._timer = null; } async getVideoPath() { if (await support_1.fs.exists(this._videoPath)) { VIDEO_FILES.add(this._videoPath); return this._videoPath; } return ''; } get isRunning() { return !!(this._process?.isRunning); } async _enforceTermination() { if (this.isRunning) { this.log.debug('Force-stopping the currently running video recording'); try { await /** @type {import('teen_process').SubProcess} */ (this._process).stop('SIGKILL'); } catch { } } this._process = null; const videoPath = await this.getVideoPath(); if (videoPath) { await support_1.fs.rimraf(videoPath); VIDEO_FILES.delete(videoPath); } return ''; } async start() { const args = [ this._udid, 'recordVideo' ]; if (this._display) { args.push('--display', this._display); } if (this._codec) { args.push('--codec', this._codec); } if (this._mask) { args.push('--mask', this._mask); } args.push('--force', this._videoPath); this._process = await new node_simctl_1.Simctl().exec('io', { args, asynchronous: true, }); this.log.debug(`Starting video recording with arguments: ${support_1.util.quote(args)}`); this._process.on('output', (stdout, stderr) => { const line = lodash_1.default.trim(stdout || stderr); if (line) { this.log.debug(`[recordVideo@${this._udid.substring(0, 8)}] ${line}`); } }); this._process.once('exit', async (code, signal) => { this._process = null; if (code === 0) { this.log.debug('Screen recording exited without errors'); } else { await this._enforceTermination(); this.log.warn(`Screen recording exited with error code ${code}, signal ${signal}`); } }); await this._process.start(0); try { await (0, asyncbox_1.waitForCondition)(async () => { if (!this.isRunning) { throw new Error(); } return !!(await this.getVideoPath()); }, { waitMs: STARTUP_TIMEOUT_MS, intervalMs: STARTUP_INTERVAL_MS, }); } catch { await this._enforceTermination(); throw this.log.errorWithException(`The expected screen record file '${this._videoPath}' does not exist after ${STARTUP_TIMEOUT_MS}ms. ` + `Check the server log for more details`); } this._timer = setTimeout(async () => { if (this.isRunning) { try { await this.stop(); } catch (e) { this.log.error(e); } } }, this._timeLimitMs); this.log.info(`The video recording has started. Will timeout in ${this._timeLimitMs}ms`); } async stop(force = false) { if (this._timer) { clearTimeout(this._timer); this._timer = null; } if (force) { return await this._enforceTermination(); } if (!this.isRunning) { this.log.debug('Screen recording is not running. Returning the recently recorded video'); return await this.getVideoPath(); } try { await /** @type {import('teen_process').SubProcess} */ (this._process).stop('SIGINT', PROCESS_SHUTDOWN_TIMEOUT_MS); } catch { await this._enforceTermination(); throw new Error(`Screen recording has failed to stop after ${PROCESS_SHUTDOWN_TIMEOUT_MS}ms`); } return await this.getVideoPath(); } } async function extractSimulatorUdid(caps) { if (caps['safari:useSimulator'] === false) { return null; } const allDevices = lodash_1.default.flatMap(lodash_1.default.values(await new node_simctl_1.Simctl().getDevices(null, 'iOS'))); for (const { name, udid, state, sdk } of allDevices) { if (state !== 'Booted') { continue; } if (lodash_1.default.toLower(caps['safari:deviceUDID']) === lodash_1.default.toLower(udid)) { return udid; } if (lodash_1.default.toLower(caps['safari:deviceName']) === lodash_1.default.toLower(name) && (caps['safari:platformVersion'] && caps['safari:platformVersion'] === sdk || !caps['safari:platformVersion'])) { return udid; } } return null; } /** * @typedef {Object} StartRecordingOptions * * @property {string} codec [hevc] - Specifies the codec type: "h264" or "hevc" * @property {string} display [internal] - Supports "internal" or "external". Default is "internal" * @property {string} mask - For non-rectangular displays, handle the mask by policy: * - ignored: The mask is ignored and the unmasked framebuffer is saved. * - alpha: Not supported, but retained for compatibility; the mask is rendered black. * - black: The mask is rendered black. * @property {string|number} timeLimit [600] - The maximum recording time, in seconds. The default * value is 600 seconds (10 minutes). * @property {boolean} forceRestart [true] - Whether to ignore the call if a screen recording is currently running * (`false`) or to start a new recording immediately and terminate the existing one if running (`true`). */ /** * Record the Simulator's display in background while the automated test is running. * This method uses `xcrun simctl io recordVideo` helper under the hood. * Check the output of `xcrun simctl io` command for more details. * * @this {SafariDriver} * @param {StartRecordingOptions} options - The available options. * @this {import('../driver').SafariDriver} * @throws {Error} If screen recording has failed to start or is not supported for the destination device. */ async function startRecordingScreen(options) { const { timeLimit, codec, display, mask, forceRestart = true, } = options ?? {}; if (this._screenRecorder?.isRunning) { this.log.info('The screen recording is already running'); if (!forceRestart) { this.log.info('Doing nothing'); return; } this.log.info('Forcing the active screen recording to stop'); await this._screenRecorder.stop(true); } this._screenRecorder = null; const udid = await extractSimulatorUdid(this.caps); if (!udid) { throw new Error('Cannot determine Simulator UDID to record the video from. ' + 'Double check your session capabilities'); } const videoPath = await support_1.tempDir.path({ prefix: support_1.util.uuidV4().substring(0, 8), suffix: DEFAULT_EXT, }); this._screenRecorder = new ScreenRecorder(udid, videoPath, this.log, { timeLimit: parseInt(`${timeLimit}`, 10), codec, display, mask, }); try { await this._screenRecorder.start(); } catch (e) { this._screenRecorder = null; throw e; } } /** * @typedef {Object} StopRecordingOptions * * @property {string} 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. * @property {string} user - The name of the user for the remote authentication. * @property {string} pass - The password for the remote authentication. * @property {string} method - The http multipart upload method name. The 'PUT' one is used by default. * @property {Object} headers - Additional headers mapping for multipart http(s) uploads * @property {string} fileFieldName [file] - The name of the form field, where the file content BLOB should be stored for * http(s) uploads * @property {Object|[string, string][]} formFields - Additional form fields for multipart http(s) uploads */ /** * Stop recording the screen. * If no screen recording has been started before then the method returns an empty string. * * @this {SafariDriver} * @param {StopRecordingOptions} options - The available options. * @returns {Promise<string>} Base64-encoded content of the recorded media file if 'remotePath' * parameter is falsy or an empty string. * @this {import('../driver').SafariDriver} * @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 stopRecordingScreen(options) { if (!this._screenRecorder) { this.log.info('No screen recording has been started. Doing nothing'); return ''; } this.log.debug('Retrieving the resulting video data'); const videoPath = await this._screenRecorder.stop(); if (!videoPath) { this.log.info('No video data is found. Returning an empty string'); return ''; } if (lodash_1.default.isEmpty(options?.remotePath)) { const { size } = await support_1.fs.stat(videoPath); this.log.debug(`The size of the resulting screen recording is ${support_1.util.toReadableSizeString(size)}`); } return await uploadRecordedMedia(videoPath, options?.remotePath, options); } /** * @typedef {import('../driver').SafariDriver} SafariDriver */ //# sourceMappingURL=record-screen.js.map