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