UNPKG

appium-xcuitest-driver

Version:

Appium driver for iOS using XCUITest for backend

444 lines 17.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getDriverInfo = exports.NATIVE_WIN = exports.DEFAULT_TIMEOUT_KEY = exports.UDID_AUTO = void 0; exports.getAndCheckXcodeVersion = getAndCheckXcodeVersion; exports.getAndCheckIosSdkVersion = getAndCheckIosSdkVersion; exports.clearLogs = clearLogs; exports.markSystemFilesForCleanup = markSystemFilesForCleanup; exports.clearSystemFiles = clearSystemFiles; exports.checkAppPresent = checkAppPresent; exports.normalizeCommandTimeouts = normalizeCommandTimeouts; exports.printUser = printUser; exports.getPIDsListeningOnPort = getPIDsListeningOnPort; exports.encodeBase64OrUpload = encodeBase64OrUpload; exports.removeAllSessionWebSocketHandlers = removeAllSessionWebSocketHandlers; exports.isLocalHost = isLocalHost; exports.normalizePlatformVersion = normalizePlatformVersion; exports.requireArgs = requireArgs; exports.assertSimulator = assertSimulator; exports.isTvOs = isTvOs; exports.normalizePlatformName = normalizePlatformName; exports.shouldSetInitialSafariUrl = shouldSetInitialSafariUrl; exports.isIos17OrNewer = isIos17OrNewer; exports.isIos18OrNewer = isIos18OrNewer; const xcode = __importStar(require("appium-xcode")); const driver_1 = require("appium/driver"); const support_1 = require("appium/support"); const bluebird_1 = __importDefault(require("bluebird")); const lodash_1 = __importDefault(require("lodash")); const node_os_1 = __importDefault(require("node:os")); const node_path_1 = __importDefault(require("node:path")); const node_url_1 = __importDefault(require("node:url")); const semver = __importStar(require("semver")); const teen_process_1 = require("teen_process"); const logger_1 = require("./logger"); const desired_caps_1 = require("./desired-caps"); exports.UDID_AUTO = 'auto'; const MODULE_NAME = 'appium-xcuitest-driver'; exports.DEFAULT_TIMEOUT_KEY = 'default'; const XCTEST_LOG_FILES_PATTERNS = [ /^Session-WebDriverAgentRunner.*\.log$/i, /^StandardOutputAndStandardError\.txt$/i, ]; const XCTEST_LOGS_CACHE_FOLDER_PREFIX = 'com.apple.dt.XCTest'; exports.NATIVE_WIN = 'NATIVE_APP'; async function getAndCheckXcodeVersion() { try { return await xcode.getVersion(true); } catch (err) { throw logger_1.log.errorWithException(`Could not determine Xcode version: ${err.message}`); } } async function getAndCheckIosSdkVersion() { try { return await xcode.getMaxIOSSDK(); } catch (err) { throw logger_1.log.errorWithException(`Could not determine iOS SDK version: ${err.message}`); } } async function clearLogs(locations) { logger_1.log.debug('Clearing log files'); const cleanupPromises = []; for (const location of locations) { if (!(await support_1.fs.exists(location))) { continue; } cleanupPromises.push((async () => { let size; try { const { stdout } = await (0, teen_process_1.exec)('du', ['-sh', location]); size = stdout.trim().split(/\s+/)[0]; } catch { } try { logger_1.log.debug(`Deleting '${location}'. ${size ? `Freeing ${size}.` : ''}`); await support_1.fs.rimraf(location); } catch (err) { logger_1.log.warn(`Unable to delete '${location}': ${err.message}`); } })()); } if (!lodash_1.default.isEmpty(cleanupPromises)) { await bluebird_1.default.all(cleanupPromises); } logger_1.log.debug('Finished clearing log files'); } // This map contains derived data logs folders as keys // and values are the count of times the particular // folder has been scheduled for removal const derivedDataCleanupMarkers = new Map(); async function markSystemFilesForCleanup(wda) { if (!wda || !(await wda.retrieveDerivedDataPath())) { logger_1.log.warn('No WebDriverAgent derived data available, so unable to mark system files for cleanup'); return; } const logsRoot = node_path_1.default.resolve(await wda.retrieveDerivedDataPath(), 'Logs'); let markersCount = 0; const existingCount = derivedDataCleanupMarkers.get(logsRoot); if (existingCount !== undefined) { markersCount = existingCount; } derivedDataCleanupMarkers.set(logsRoot, ++markersCount); } async function clearSystemFiles(wda) { // only want to clear the system files for the particular WDA xcode run if (!wda || !(await wda.retrieveDerivedDataPath())) { logger_1.log.warn('No WebDriverAgent derived data available, so unable to clear system files'); return; } const logsRoot = node_path_1.default.resolve(await wda.retrieveDerivedDataPath(), 'Logs'); const existingCount = derivedDataCleanupMarkers.get(logsRoot); if (existingCount !== undefined) { let markersCount = existingCount; derivedDataCleanupMarkers.set(logsRoot, --markersCount); if (markersCount > 0) { logger_1.log.info(`Not cleaning '${logsRoot}' folder, because the other session does not expect it to be cleaned`); return; } } derivedDataCleanupMarkers.set(logsRoot, 0); // Cleaning up big temporary files created by XCTest: https://github.com/appium/appium/issues/9410 const globPattern = `${node_os_1.default.tmpdir()}/${XCTEST_LOGS_CACHE_FOLDER_PREFIX}*/`; const dstFolders = await support_1.fs.glob(globPattern); if (lodash_1.default.isEmpty(dstFolders)) { logger_1.log.debug(`Did not find the temporary XCTest logs root at '${globPattern}'`); } else { // perform the cleanup asynchronously const promises = []; for (const dstFolder of dstFolders) { const promise = (async () => { try { await support_1.fs.walkDir(dstFolder, true, (itemPath, isDir) => { if (isDir) { return; } const fileName = node_path_1.default.basename(itemPath); if (XCTEST_LOG_FILES_PATTERNS.some((p) => p.test(fileName))) { support_1.fs.rimraf(itemPath); } }); } catch (e) { logger_1.log.debug(e.stack); logger_1.log.info(e.message); } })(); promises.push(promise); } logger_1.log.debug(`Started XCTest logs cleanup in '${dstFolders}'`); if (promises.length) { await bluebird_1.default.all(promises); } } if (await support_1.fs.exists(logsRoot)) { logger_1.log.info(`Cleaning test logs in '${logsRoot}' folder`); await clearLogs([logsRoot]); return; } logger_1.log.info(`There is no ${logsRoot} folder, so not cleaning files`); } async function checkAppPresent(app) { logger_1.log.debug(`Checking whether app '${app}' is actually present on file system`); if (!(await support_1.fs.exists(app))) { throw logger_1.log.errorWithException(`Could not find app at '${app}'`); } logger_1.log.debug('App is present'); } /** * Reads the content to the current module's package.json * * @returns The full path to module root * @throws If the current module's package.json cannot be determined */ const getModuleManifest = lodash_1.default.memoize(async function getModuleManifest() { // Start from the directory containing the compiled output (build/lib) or source (lib) // and walk up to find package.json let currentDir = node_path_1.default.resolve(__dirname, '..'); let isAtFsRoot = false; while (!isAtFsRoot) { const manifestPath = node_path_1.default.join(currentDir, 'package.json'); try { if (await support_1.fs.exists(manifestPath)) { const manifest = JSON.parse(await support_1.fs.readFile(manifestPath, 'utf8')); if (manifest.name === MODULE_NAME) { return manifest; } } } catch { } const parentDir = node_path_1.default.dirname(currentDir); isAtFsRoot = currentDir.length <= parentDir.length; currentDir = parentDir; } throw new Error(`Cannot find the package manifest of the ${MODULE_NAME} Node.js module`); }); /** * @returns */ exports.getDriverInfo = lodash_1.default.memoize(async function getDriverInfo() { const [stat, manifest] = await bluebird_1.default.all([ support_1.fs.stat(node_path_1.default.resolve(__dirname, '..')), getModuleManifest(), ]); return { built: stat.mtime.toString(), version: manifest.version, }; }); function normalizeCommandTimeouts(value) { // The value is normalized already if (typeof value !== 'string') { return value; } let result = {}; // Use as default timeout for all commands if a single integer value is provided if (!isNaN(Number(value))) { result[exports.DEFAULT_TIMEOUT_KEY] = lodash_1.default.toInteger(value); return result; } // JSON object has been provided. Let's parse it try { result = JSON.parse(value); if (!lodash_1.default.isPlainObject(result)) { throw new Error(); } } catch { throw logger_1.log.errorWithException(`"commandTimeouts" capability should be a valid JSON object. "${value}" was given instead`); } for (const [cmd, timeout] of lodash_1.default.toPairs(result)) { if (!lodash_1.default.isInteger(timeout) || timeout <= 0) { throw logger_1.log.errorWithException(`The timeout for "${cmd}" should be a valid natural number of milliseconds. "${timeout}" was given instead`); } } return result; } async function printUser() { try { const { stdout } = await (0, teen_process_1.exec)('whoami'); logger_1.log.debug(`Current user: '${stdout.trim()}'`); } catch (err) { logger_1.log.debug(`Unable to get username running server: ${err.message}`); } } /** * Get the IDs of processes listening on the particular system port. * It is also possible to apply additional filtering based on the * process command line. * * @param port - The port number. * @param filteringFunc - Optional lambda function, which * receives command line string of the particular process * listening on given port, and is expected to return * either true or false to include/exclude the corresponding PID * from the resulting array. * @returns - the list of matched process ids. */ async function getPIDsListeningOnPort(port, filteringFunc = null) { const result = []; try { // This only works since Mac OS X El Capitan const { stdout } = await (0, teen_process_1.exec)('lsof', ['-ti', `tcp:${port}`]); result.push(...stdout.trim().split(/\n+/)); } catch { return result; } if (!lodash_1.default.isFunction(filteringFunc)) { return result; } return await bluebird_1.default.filter(result, async (x) => { const { stdout } = await (0, teen_process_1.exec)('ps', ['-p', x, '-o', 'command']); return await filteringFunc(stdout); }); } /** * Encodes the given local file to base64 and returns the resulting string * or uploads it to a remote server using http/https or ftp protocols * if `remotePath` is set * * @param localPath - The path to an existing local file * @param remotePath - The path to the remote location, where * this file should be uploaded * @param uploadOptions - Set of upload options * @returns Either an empty string if the upload was successful or * base64-encoded file representation if `remotePath` is falsy */ async function encodeBase64OrUpload(localPath, remotePath = null, uploadOptions = {}) { if (!(await support_1.fs.exists(localPath))) { throw logger_1.log.errorWithException(`The file at '${localPath}' does not exist or is not accessible`); } if (lodash_1.default.isEmpty(remotePath)) { const { size } = await support_1.fs.stat(localPath); logger_1.log.debug(`The size of the file is ${support_1.util.toReadableSizeString(size)}`); return (await support_1.util.toInMemoryBase64(localPath)).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(localPath, remotePath, options); return ''; } /** * Stops and removes all web socket handlers that are listening * in scope of the current session. * * @this {XCUITestDriver} * @returns */ async function removeAllSessionWebSocketHandlers() { if (!this.sessionId || !lodash_1.default.isFunction(this.server?.getWebSocketHandlers)) { return; } const activeHandlers = await this.server.getWebSocketHandlers(this.sessionId); for (const pathname of lodash_1.default.keys(activeHandlers)) { await this.server.removeWebSocketHandler(pathname); } } /** * Returns true if the urlString is localhost * @param urlString * @returns Return true if the urlString is localhost */ function isLocalHost(urlString) { try { const hostname = node_url_1.default.parse(urlString).hostname; return ['localhost', '127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(hostname); } catch { logger_1.log.warn(`'${urlString}' cannot be parsed as a valid URL`); } return false; } /** * Normalizes platformVersion to a valid iOS version string * * @param originalVersion - Loose version number, that can be parsed by semver * @return iOS version number in <major>.<minor> format * @throws if the version number cannot be parsed */ function normalizePlatformVersion(originalVersion) { const normalizedVersion = semver.coerce(originalVersion); if (!normalizedVersion) { throw new Error(`The platform version '${originalVersion}' should be a valid version number`); } return `${normalizedVersion.major}.${normalizedVersion.minor}`; } /** * Assert the presence of particular keys in the given object * * @param argNames one or more key names * @param opts the object to check * @returns the same given object */ function requireArgs(argNames, opts = {}) { for (const argName of lodash_1.default.isArray(argNames) ? argNames : [argNames]) { if (!lodash_1.default.has(opts, argName)) { throw new driver_1.errors.InvalidArgumentError(`'${argName}' argument must be provided`); } } return opts; } /** * Asserts that the given driver is running on a Simulator and return * the simlator instance. * * @param action - Description of action */ function assertSimulator(action) { if (!this.isSimulator()) { throw new Error(`${lodash_1.default.upperFirst(action)} can only be performed on Simulator`); } return this.device; } /** * Check if platform name is the TV OS one. */ function isTvOs(platformName) { return lodash_1.default.toLower(platformName ?? '') === lodash_1.default.toLower(desired_caps_1.PLATFORM_NAME_TVOS); } /** * Return normalized platform name. */ function normalizePlatformName(platformName) { return isTvOs(platformName) ? desired_caps_1.PLATFORM_NAME_TVOS : desired_caps_1.PLATFORM_NAME_IOS; } function shouldSetInitialSafariUrl(opts) { return !(opts.safariInitialUrl === '' || (opts.noReset && lodash_1.default.isNil(opts.safariInitialUrl))) && !opts.initialDeeplinkUrl; } function isIos17OrNewer(opts) { return !!opts.platformVersion && support_1.util.compareVersions(opts.platformVersion, '>=', '17.0'); } function isIos18OrNewer(opts) { return !!opts.platformVersion && support_1.util.compareVersions(opts.platformVersion, '>=', '18.0'); } //# sourceMappingURL=utils.js.map