UNPKG

appium-android-driver

Version:

Android UiAutomator and Chrome support for Appium

220 lines (202 loc) 6.99 kB
import {DEFAULT_WS_PATHNAME_PREFIX, BaseDriver} from 'appium/driver'; import _ from 'lodash'; import os from 'node:os'; import WebSocket from 'ws'; import { GET_SERVER_LOGS_FEATURE, toLogRecord, nativeLogEntryToSeleniumEntry, } from '../utils'; import { NATIVE_WIN } from './context/helpers'; import { BIDI_EVENT_NAME } from './bidi/constants'; import { makeLogEntryAddedEvent } from './bidi/models'; export const supportedLogTypes = { logcat: { description: 'Logs for Android applications on real device and emulators via ADB', /** * * @param {import('../driver').AndroidDriver} self * @returns */ getter: (self) => /** @type {ADB} */ (self.adb).getLogcatLogs(), }, bugreport: { description: `'adb bugreport' output for advanced issues diagnostic`, /** * * @param {import('../driver').AndroidDriver} self * @returns */ getter: async (self) => { const output = await /** @type {ADB} */ (self.adb).bugreport(); const timestamp = Date.now(); return output.split(os.EOL).map((x) => toLogRecord(timestamp, x)); }, }, server: { description: 'Appium server logs', /** * * @param {import('../driver').AndroidDriver} self * @returns */ getter: (self) => { self.assertFeatureEnabled(GET_SERVER_LOGS_FEATURE); return self.log.unwrap().record.map(nativeLogEntryToSeleniumEntry); }, }, }; /** * Starts Android logcat broadcast websocket on the same host and port * where Appium server is running at `/ws/session/:sessionId:/appium/logcat` endpoint. The method * will return immediately if the web socket is already listening. * * Each connected websocket listener will receive logcat log lines * as soon as they are visible to Appium. * * @this {import('../driver').AndroidDriver} * @returns {Promise<void>} */ export async function mobileStartLogsBroadcast() { const server = /** @type {import('@appium/types').AppiumServer} */ (this.server); const pathname = WEBSOCKET_ENDPOINT(/** @type {string} */ (this.sessionId)); if (!_.isEmpty(await server.getWebSocketHandlers(pathname))) { this.log.debug(`The logcat broadcasting web socket server is already listening at ${pathname}`); return; } this.log.info( `Starting logcat broadcasting on web socket server ` + `${JSON.stringify(server.address())} to ${pathname}`, ); // https://github.com/websockets/ws/blob/master/doc/ws.md const wss = new WebSocket.Server({ noServer: true, }); wss.on('connection', (ws, req) => { if (req) { const remoteIp = _.isEmpty(req.headers['x-forwarded-for']) ? req.connection?.remoteAddress : req.headers['x-forwarded-for']; this.log.debug(`Established a new logcat listener web socket connection from ${remoteIp}`); } else { this.log.debug('Established a new logcat listener web socket connection'); } if (_.isEmpty(this._logcatWebsocketListener)) { this._logcatWebsocketListener = (logRecord) => { if (ws?.readyState === WebSocket.OPEN) { ws.send(logRecord.message); } }; } this.adb.setLogcatListener(this._logcatWebsocketListener); ws.on('close', (code, reason) => { if (!_.isEmpty(this._logcatWebsocketListener)) { try { this.adb.removeLogcatListener(this._logcatWebsocketListener); } catch {} this._logcatWebsocketListener = undefined; } let closeMsg = 'Logcat listener web socket is closed.'; if (!_.isEmpty(code)) { closeMsg += ` Code: ${code}.`; } if (!_.isEmpty(reason)) { closeMsg += ` Reason: ${reason.toString()}.`; } this.log.debug(closeMsg); }); }); await server.addWebSocketHandler(pathname, /** @type {import('@appium/types').WSServer} */ (wss)); } /** * Stops the previously started logcat broadcasting wesocket server. * This method will return immediately if no server is running. * * @this {import('../driver').AndroidDriver} * @returns {Promise<void>} */ export async function mobileStopLogsBroadcast() { const pathname = WEBSOCKET_ENDPOINT(/** @type {string} */ (this.sessionId)); const server = /** @type {import('@appium/types').AppiumServer} */ (this.server); if (_.isEmpty(await server.getWebSocketHandlers(pathname))) { return; } this.log.debug( `Stopping logcat broadcasting on web socket server ` + `${JSON.stringify(server.address())} to ${pathname}`, ); await server.removeWebSocketHandler(pathname); } /** * @this {import('../driver').AndroidDriver} * @returns {Promise<string[]>} */ export async function getLogTypes() { // XXX why doesn't `super` work here? const nativeLogTypes = await BaseDriver.prototype.getLogTypes.call(this); if (this.isWebContext()) { const webLogTypes = /** @type {string[]} */ ( await /** @type {import('appium-chromedriver').Chromedriver} */ ( this.chromedriver ).jwproxy.command('/log/types', 'GET') ); return [...nativeLogTypes, ...webLogTypes]; } return nativeLogTypes; } /** * https://w3c.github.io/webdriver-bidi/#event-log-entryAdded * * @template {import('node:events').EventEmitter} EE * @this {import('../driver').AndroidDriver} * @param {EE} logEmitter * @param {BiDiListenerProperties} properties * @returns {[EE, LogListener]} */ export function assignBiDiLogListener (logEmitter, properties) { const { type, context = NATIVE_WIN, srcEventName = 'output', entryTransformer, } = properties; const listener = (/** @type {import('../utils').LogEntry} */ logEntry) => { const finalEntry = entryTransformer ? entryTransformer(logEntry) : logEntry; this.eventEmitter.emit(BIDI_EVENT_NAME, makeLogEntryAddedEvent(finalEntry, context, type)); }; logEmitter.on(srcEventName, listener); return [logEmitter, listener]; } /** * @this {import('../driver').AndroidDriver} * @param {string} logType * @returns {Promise<any>} */ export async function getLog(logType) { if (this.isWebContext() && !_.keys(this.supportedLogTypes).includes(logType)) { return await /** @type {import('appium-chromedriver').Chromedriver} */ ( this.chromedriver ).jwproxy.command('/log', 'POST', {type: logType}); } // XXX why doesn't `super` work here? return await BaseDriver.prototype.getLog.call(this, logType); } // #region Internal helpers /** * @param {string} sessionId * @returns {string} */ const WEBSOCKET_ENDPOINT = (sessionId) => `${DEFAULT_WS_PATHNAME_PREFIX}/session/${sessionId}/appium/device/logcat`; // #endregion /** * @typedef {import('appium-adb').ADB} ADB */ /** * @typedef {Object} BiDiListenerProperties * @property {string} type * @property {string} [srcEventName='output'] * @property {string} [context=NATIVE_WIN] * @property {(x: Object) => import('../utils').LogEntry} [entryTransformer] */ /** @typedef {(logEntry: import('../utils').LogEntry) => any} LogListener */