appium-safari-driver
Version:
Appium driver for Safari browser
315 lines • 12.4 kB
JavaScript
"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