appium-xcuitest-driver
Version:
Appium driver for iOS using XCUITest for backend
266 lines • 11.9 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.supportedLogTypes = void 0;
exports.extractLogs = extractLogs;
exports.startLogCapture = startLogCapture;
exports.mobileStartLogsBroadcast = mobileStartLogsBroadcast;
exports.mobileStopLogsBroadcast = mobileStopLogsBroadcast;
exports.assignBiDiLogListener = assignBiDiLogListener;
const lodash_1 = __importDefault(require("lodash"));
const bluebird_1 = __importDefault(require("bluebird"));
const driver_1 = require("appium/driver");
const ios_crash_log_1 = require("../device/log/ios-crash-log");
const ios_simulator_log_1 = require("../device/log/ios-simulator-log");
const ios_device_log_1 = require("../device/log/ios-device-log");
const ws_1 = __importDefault(require("ws"));
const safari_console_log_1 = require("../device/log/safari-console-log");
const safari_network_log_1 = require("../device/log/safari-network-log");
const helpers_1 = require("../device/log/helpers");
const utils_1 = require("../utils");
const constants_1 = require("./bidi/constants");
const models_1 = require("./bidi/models");
/**
* Determines the websocket endpoint based on the `sessionId`
*/
const WEBSOCKET_ENDPOINT = (sessionId) => `${driver_1.DEFAULT_WS_PATHNAME_PREFIX}/session/${sessionId}/appium/device/syslog`;
const COLOR_CODE_PATTERN = /\u001b\[(\d+(;\d+)*)?m/g; // eslint-disable-line no-control-regex
const GET_SERVER_LOGS_FEATURE = 'get_server_logs';
/**
* @privateRemarks The return types for these getters should be specified
*/
const SUPPORTED_LOG_TYPES = {
syslog: {
description: 'System Logs - Device logs for iOS applications on real devices and simulators',
getter: async (self) => await self.extractLogs('syslog', self.logs),
},
crashlog: {
description: 'Crash Logs - Crash reports for iOS applications on real devices and simulators',
getter: async (self) => await self.extractLogs('crashlog', self.logs),
},
performance: {
description: 'Performance Logs - Debug Timelines on real devices and simulators',
getter: async (self) => await self.extractLogs('performance', self.logs),
},
safariConsole: {
description: 'Safari Console Logs - data written to the JS console in Safari',
getter: async (self) => await self.extractLogs('safariConsole', self.logs),
},
safariNetwork: {
description: 'Safari Network Logs - information about network operations undertaken by Safari',
getter: async (self) => await self.extractLogs('safariNetwork', self.logs),
},
server: {
description: 'Appium server logs',
getter: (self) => {
self.assertFeatureEnabled(GET_SERVER_LOGS_FEATURE);
return self.log.unwrap().record.map(nativeLogEntryToSeleniumEntry);
},
},
};
const LOG_NAMES_TO_CAPABILITY_NAMES_MAP = {
safariConsole: 'showSafariConsoleLog',
safariNetwork: 'showSafariNetworkLog',
enablePerformanceLogging: 'enablePerformanceLogging',
};
exports.supportedLogTypes = SUPPORTED_LOG_TYPES;
/**
* Extracts logs of the specified type from the logs container.
*
* @param logType - The type of log to extract
* @param logsContainer - Container holding log objects
* @returns The extracted logs
* @throws {Error} If logs are not available or the log type is not found
*/
async function extractLogs(logType, logsContainer = {}) {
// make sure that we have logs at all
// otherwise it's not been initialized
if (lodash_1.default.isEmpty(logsContainer)) {
throw new Error('No logs currently available. Is the device/simulator started?');
}
// If logs captured successfully send response with data, else send error
const logObject = logsContainer[logType];
if (logObject) {
return await logObject.getLogs();
}
if (logType in LOG_NAMES_TO_CAPABILITY_NAMES_MAP) {
throw new Error(`${logType} logs are not enabled. Make sure you've set a proper value ` +
`to the 'appium:${LOG_NAMES_TO_CAPABILITY_NAMES_MAP[logType]}' capability.`);
}
throw new Error(`No logs of type '${logType}' found. Supported log types are: ${lodash_1.default.keys(SUPPORTED_LOG_TYPES)}.`);
}
/**
* Starts capturing iOS system logs.
*
* Initializes and starts capturing syslog and crashlog. Optionally starts Safari console and network logs
* if the corresponding capabilities are enabled.
*
* @returns `true` if syslog capture started successfully; `false` otherwise
*/
async function startLogCapture() {
this.logs = this.logs || {};
if (!lodash_1.default.isUndefined(this.logs.syslog) && this.logs.syslog.isCapturing) {
this.log.warn('Trying to start iOS log capture but it has already started!');
return true;
}
if (lodash_1.default.isUndefined(this.logs.syslog)) {
[this.logs.crashlog,] = assignBiDiLogListener.bind(this)(new ios_crash_log_1.IOSCrashLog({
sim: this.device,
udid: this.isRealDevice() ? this.opts.udid : undefined,
log: this.log,
}), {
type: 'crashlog',
});
[this.logs.syslog,] = assignBiDiLogListener.bind(this)(this.isRealDevice()
? new ios_device_log_1.IOSDeviceLog({
udid: this.opts.udid,
showLogs: this.opts.showIOSLog,
log: this.log,
})
: new ios_simulator_log_1.IOSSimulatorLog({
sim: this.device,
showLogs: this.opts.showIOSLog,
iosSimulatorLogsPredicate: this.opts.iosSimulatorLogsPredicate,
simulatorLogLevel: this.opts.simulatorLogLevel,
log: this.log,
iosSyslogFile: this.opts.iosSyslogFile
}), {
type: 'syslog',
});
if (lodash_1.default.isBoolean(this.opts.showSafariConsoleLog)) {
[this.logs.safariConsole,] = assignBiDiLogListener.bind(this)(new safari_console_log_1.SafariConsoleLog({
showLogs: this.opts.showSafariConsoleLog,
log: this.log,
}), {
type: 'safariConsole',
});
}
if (lodash_1.default.isBoolean(this.opts.showSafariNetworkLog)) {
[this.logs.safariNetwork,] = assignBiDiLogListener.bind(this)(new safari_network_log_1.SafariNetworkLog({
showLogs: this.opts.showSafariNetworkLog,
log: this.log,
}), {
type: 'safariNetwork',
});
}
if (this.isFeatureEnabled(GET_SERVER_LOGS_FEATURE)) {
[, this._bidiServerLogListener] = assignBiDiLogListener.bind(this)(this.log.unwrap(), {
type: 'server',
srcEventName: 'log',
entryTransformer: nativeLogEntryToSeleniumEntry,
});
}
}
let didStartSyslog = false;
const promises = [
(async () => {
try {
await this.logs.syslog?.startCapture();
didStartSyslog = true;
this.eventEmitter.emit('syslogStarted', this.logs.syslog);
}
catch (err) {
this.log.debug(err.stack);
this.log.warn(`Continuing without capturing device logs: ${err.message}`);
}
})(),
this.logs.crashlog?.startCapture() ?? bluebird_1.default.resolve(),
];
await bluebird_1.default.all(promises);
return didStartSyslog;
}
/**
* Starts an iOS system logs broadcast websocket.
*
* The websocket listens on the same host and port as Appium. The endpoint created is `/ws/session/:sessionId:/appium/syslog`.
*
* If the websocket is already running, this command does nothing.
*
* Each connected websocket listener will receive syslog lines as soon as they are visible to Appium.
* @see https://appiumpro.com/editions/55-using-mobile-execution-commands-to-continuously-stream-device-logs-with-appium
*/
async function mobileStartLogsBroadcast() {
const pathname = WEBSOCKET_ENDPOINT(this.sessionId);
if (!lodash_1.default.isEmpty(await this.server.getWebSocketHandlers(pathname))) {
this.log.debug(`The system logs broadcasting web socket server is already listening at ${pathname}`);
return;
}
this.log.info(`Assigning system logs broadcasting web socket server to ${pathname}`);
// https://github.com/websockets/ws/blob/master/doc/ws.md
const wss = new ws_1.default.Server({
noServer: true,
});
wss.on('connection', (ws, req) => {
if (req) {
const remoteIp = lodash_1.default.isEmpty(req.headers['x-forwarded-for'])
? req.connection?.remoteAddress
: req.headers['x-forwarded-for'];
this.log.debug(`Established a new system logs listener web socket connection from ${remoteIp}`);
}
else {
this.log.debug('Established a new system logs listener web socket connection');
}
if (lodash_1.default.isEmpty(this._syslogWebsocketListener)) {
this._syslogWebsocketListener = (logRecord) => {
if (ws?.readyState === ws_1.default.OPEN) {
ws.send(logRecord.message);
}
};
}
this.logs.syslog?.on('output', this._syslogWebsocketListener);
ws.on('close', (code, reason) => {
if (!lodash_1.default.isEmpty(this._syslogWebsocketListener)) {
this.logs.syslog?.removeListener('output', this._syslogWebsocketListener);
this._syslogWebsocketListener = null;
}
let closeMsg = 'System logs listener web socket is closed.';
if (!lodash_1.default.isEmpty(code)) {
closeMsg += ` Code: ${code}.`;
}
if (!lodash_1.default.isEmpty(reason)) {
closeMsg += ` Reason: ${reason.toString()}.`;
}
this.log.debug(closeMsg);
});
});
await this.server.addWebSocketHandler(pathname, wss);
}
/**
* Stops the syslog broadcasting websocket server previously started by `mobile: startLogsBroadcast`.
*
* If no websocket server is running, this command does nothing.
*/
async function mobileStopLogsBroadcast() {
const pathname = WEBSOCKET_ENDPOINT(this.sessionId);
if (lodash_1.default.isEmpty(await this.server.getWebSocketHandlers(pathname))) {
return;
}
this.log.debug('Stopping the system logs broadcasting web socket server');
await this.server.removeWebSocketHandler(pathname);
}
/**
* Assigns a BiDi log listener to the given log emitter.
*
* https://w3c.github.io/webdriver-bidi/#event-log-entryAdded
*
* @template EE extends EventEmitter
* @param logEmitter - The event emitter to attach the listener to
* @param properties - Configuration for the BiDi listener
* @returns A tuple containing the log emitter and the listener function
*/
function assignBiDiLogListener(logEmitter, properties) {
const { type, context = utils_1.NATIVE_WIN, srcEventName = 'output', entryTransformer, } = properties;
const listener = (logEntry) => {
const finalEntry = entryTransformer ? entryTransformer(logEntry) : logEntry;
this.eventEmitter.emit(constants_1.BIDI_EVENT_NAME, (0, models_1.makeLogEntryAddedEvent)(finalEntry, context, type));
};
logEmitter.on(srcEventName, listener);
return [logEmitter, listener];
}
function nativeLogEntryToSeleniumEntry(x) {
const msg = lodash_1.default.isEmpty(x.prefix) ? x.message : `[${x.prefix}] ${x.message}`;
return (0, helpers_1.toLogEntry)(lodash_1.default.replace(msg, COLOR_CODE_PATTERN, ''), x.timestamp ?? Date.now());
}
//# sourceMappingURL=log.js.map