UNPKG

appium-safari-driver

Version:
269 lines 9.57 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ScreenRecorder = void 0; exports.startRecordingScreen = startRecordingScreen; exports.stopRecordingScreen = stopRecordingScreen; 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'; async function uploadRecordedMedia(localFile, remotePath = null, uploadOptions = {}) { if (!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 { log; _process = null; _udid; _videoPath; _codec; _display; _mask; _timeLimitMs = DEFAULT_TIME_LIMIT_MS; _timer = null; constructor(udid, videoPath, log, opts = {}) { this.log = log; this._udid = udid; this._videoPath = videoPath; this._codec = opts.codec; this._display = opts.display; this._mask = opts.mask; if (opts.timeLimit) { const timeLimitMs = parseInt(String(opts.timeLimit), 10); if (timeLimitMs > 0) { this._timeLimitMs = timeLimitMs * 1000; } } } get isRunning() { return !!this._process?.isRunning; } async getVideoPath() { if (await support_1.fs.exists(this._videoPath)) { VIDEO_FILES.add(this._videoPath); return this._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 = (stdout || stderr)?.trim() ?? ''; 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(); } if (!this._process) { throw new Error('Screen recording process is not available'); } try { await 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 _enforceTermination() { if (this.isRunning && this._process) { this.log.debug('Force-stopping the currently running video recording'); try { await 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 ''; } } exports.ScreenRecorder = ScreenRecorder; /** * 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. * * @param options - The available options. * @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; } } /** * Stop recording the screen. * If no screen recording has been started before then the method returns an empty string. * * @param options - The available options. * @returns Base64-encoded content of the recorded media file if 'remotePath' * parameter is falsy or an empty string. * @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 (!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 ?? null, options ?? {}); } async function extractSimulatorUdid(caps) { if (caps['safari:useSimulator'] === false) { return null; } const allDevices = Object.values(await new node_simctl_1.Simctl().getDevices(null, 'iOS')).flat(); for (const { name, udid, state, sdk } of allDevices) { if (state !== 'Booted') { continue; } if (caps['safari:deviceUDID']?.toLowerCase() === udid.toLowerCase()) { return udid; } if (caps['safari:deviceName']?.toLowerCase() === name.toLowerCase() && ((caps['safari:platformVersion'] && caps['safari:platformVersion'] === sdk) || !caps['safari:platformVersion'])) { return udid; } } return null; } //# sourceMappingURL=record-screen.js.map