appium-xcuitest-driver
Version:
Appium driver for iOS using XCUITest for backend
540 lines • 21.9 kB
JavaScript
;
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.DEFAULT_TIMEOUT_KEY = exports.getDriverInfo = exports.NATIVE_WIN = exports.UDID_AUTO = void 0;
exports.assertSimulator = assertSimulator;
exports.isTvOs = isTvOs;
exports.normalizePlatformName = normalizePlatformName;
exports.shouldSetInitialSafariUrl = shouldSetInitialSafariUrl;
exports.isIos17OrNewer = isIos17OrNewer;
exports.getAndCheckXcodeVersion = getAndCheckXcodeVersion;
exports.getAndCheckIosSdkVersion = getAndCheckIosSdkVersion;
exports.checkAppPresent = checkAppPresent;
exports.clearSystemFiles = clearSystemFiles;
exports.translateDeviceName = translateDeviceName;
exports.normalizeCommandTimeouts = normalizeCommandTimeouts;
exports.markSystemFilesForCleanup = markSystemFilesForCleanup;
exports.printUser = printUser;
exports.getPIDsListeningOnPort = getPIDsListeningOnPort;
exports.encodeBase64OrUpload = encodeBase64OrUpload;
exports.removeAllSessionWebSocketHandlers = removeAllSessionWebSocketHandlers;
exports.isLocalHost = isLocalHost;
exports.normalizePlatformVersion = normalizePlatformVersion;
exports.clearLogs = clearLogs;
exports.requireArgs = requireArgs;
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 ios_generic_simulators_1 = __importDefault(require("./ios-generic-simulators"));
const logger_1 = __importDefault(require("./logger"));
const desired_caps_1 = require("./desired-caps");
exports.UDID_AUTO = 'auto';
const MODULE_NAME = 'appium-xcuitest-driver';
const DEFAULT_TIMEOUT_KEY = 'default';
exports.DEFAULT_TIMEOUT_KEY = DEFAULT_TIMEOUT_KEY;
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';
/**
* @privateRemarks Is the minimum version really Xcode 7.3?
* @returns {Promise<XcodeVersion>}
*/
async function getAndCheckXcodeVersion() {
/** @type {XcodeVersion} */
let version;
try {
version = /** @type {XcodeVersion} */ (await xcode.getVersion(true));
}
catch (err) {
logger_1.default.error(err);
throw new Error(`Could not determine Xcode version: ${err.message}`);
}
// we do not support Xcodes < 7.3,
if (version.versionFloat < 7.3) {
const msg = `Xcode ${version.versionString} is not supported. Please upgrade to version 7.3 or higher`;
logger_1.default.error(msg);
throw new Error(msg);
}
return version;
}
/**
* @returns {Promise<string|null>}
*/
async function getAndCheckIosSdkVersion() {
try {
return await xcode.getMaxIOSSDK();
}
catch (err) {
throw logger_1.default.errorWithException(`Could not determine iOS SDK version: ${err.message}`);
}
}
/**
* Get the generic simulator for a given IOS version and device type (iPhone, iPad)
*
* @param {string|number} platformVersion IOS version. e.g.) 13.0
* @param {string} deviceName Type of IOS device. Can be iPhone, iPad (possibly more in the future)
*
* @returns {string?} Generic iPhone or iPad simulator (if applicable)
*/
function getGenericSimulatorForIosVersion(platformVersion, deviceName) {
const genericSimulators = ios_generic_simulators_1.default[deviceName];
if (!genericSimulators) {
return null;
}
// Find the highest iOS version in the list that is below the provided version
let result = null;
const compareVersions = ([simOne], [simTwo]) => support_1.util.compareVersions(simOne, '<', simTwo) ? -1 : 1;
for (const [platformVersionFromList, iosSimulator] of genericSimulators.sort(compareVersions)) {
if (support_1.util.compareVersions(platformVersionFromList, '>', String(platformVersion))) {
break;
}
result = iosSimulator;
}
return result;
}
/**
*
* @param {string} platformVersion
* @param {string} deviceName
* @returns {string}
*/
function translateDeviceName(platformVersion, deviceName) {
if (!deviceName) {
return deviceName;
}
const deviceNameTranslated = getGenericSimulatorForIosVersion(platformVersion, `${deviceName}`.toLowerCase().trim());
if (!deviceNameTranslated) {
return deviceName;
}
logger_1.default.debug(`Changing deviceName from '${deviceName}' to '${deviceNameTranslated}'`);
return deviceNameTranslated;
}
/**
* @param {string[]} locations
* @returns {Promise<void>}
*/
async function clearLogs(locations) {
logger_1.default.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.default.debug(`Deleting '${location}'. ${size ? `Freeing ${size}.` : ''}`);
await support_1.fs.rimraf(location);
}
catch (err) {
logger_1.default.warn(`Unable to delete '${location}': ${err.message}`);
}
})());
}
if (!lodash_1.default.isEmpty(cleanupPromises)) {
await bluebird_1.default.all(cleanupPromises);
}
logger_1.default.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.default.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;
if (derivedDataCleanupMarkers.has(logsRoot)) {
markersCount = derivedDataCleanupMarkers.get(logsRoot);
}
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.default.warn('No WebDriverAgent derived data available, so unable to clear system files');
return;
}
const logsRoot = node_path_1.default.resolve(await wda.retrieveDerivedDataPath(), 'Logs');
if (derivedDataCleanupMarkers.has(logsRoot)) {
let markersCount = derivedDataCleanupMarkers.get(logsRoot);
derivedDataCleanupMarkers.set(logsRoot, --markersCount);
if (markersCount > 0) {
logger_1.default.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.default.debug(`Did not find the temporary XCTest logs root at '${globPattern}'`);
}
else {
// perform the cleanup asynchronously
/** @type {Promise[]} */
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.default.debug(e.stack);
logger_1.default.info(e.message);
}
})();
promises.push(promise);
}
logger_1.default.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.default.info(`Cleaning test logs in '${logsRoot}' folder`);
await clearLogs([logsRoot]);
return;
}
logger_1.default.info(`There is no ${logsRoot} folder, so not cleaning files`);
}
async function checkAppPresent(app) {
logger_1.default.debug(`Checking whether app '${app}' is actually present on file system`);
if (!(await support_1.fs.exists(app))) {
throw logger_1.default.errorWithException(`Could not find app at '${app}'`);
}
logger_1.default.debug('App is present');
}
/**
* Reads the content to the current module's package.json
*
* @returns {Promise<Record<string, any>>} The full path to module root
* @throws {Error} If the current module's package.json cannot be determined
*/
const getModuleManifest = lodash_1.default.memoize(async function getModuleManifest() {
let currentDir = node_path_1.default.dirname(node_path_1.default.resolve(__filename));
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 { }
currentDir = node_path_1.default.dirname(currentDir);
isAtFsRoot = currentDir.length <= node_path_1.default.dirname(currentDir).length;
}
throw new Error(`Cannot find the package manifest of the ${MODULE_NAME} Node.js module`);
});
/**
* @typedef {Object} DriverInfo
* @property {string} version Driver version string
* @property {string} built Driver build timestamp
*/
/**
* @returns {DriverInfo}
*/
const 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,
};
});
exports.getDriverInfo = getDriverInfo;
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[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.default.errorWithException(`"commandTimeouts" capability should be a valid JSON object. "${value}" was given instead`);
}
for (let [cmd, timeout] of lodash_1.default.toPairs(result)) {
if (!lodash_1.default.isInteger(timeout) || timeout <= 0) {
throw logger_1.default.errorWithException(`The timeout for "${cmd}" should be a valid natural number of milliseconds. "${timeout}" was given instead`);
}
}
return result;
}
async function printUser() {
try {
let { stdout } = await (0, teen_process_1.exec)('whoami');
logger_1.default.debug(`Current user: '${stdout.trim()}'`);
}
catch (err) {
logger_1.default.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 {string|number} port - The port number.
* @param {?Function} 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 {Promise<string[]>} - 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);
});
}
/**
* @typedef {Object} UploadOptions
*
* @property {string} [user] - The name of the user for the remote authentication. Only works if `remotePath` is provided.
* @property {string} [pass] - The password for the remote authentication. Only works if `remotePath` is provided.
* @property {import('axios').Method} [method] - The http multipart upload method name. The 'PUT' one is used by default.
* Only works if `remotePath` is provided.
* @property {import('@appium/types').HTTPHeaders} [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 {Record<string, any> | [string, any][]} [formFields] - Additional form fields for multipart http(s) uploads
*/
/**
* 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 {string} localPath - The path to an existing local file
* @param {string|null} [remotePath] - The path to the remote location, where
* this file should be uploaded
* @param {UploadOptions} uploadOptions - Set of upload options
* @returns {Promise<string>} 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.default.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.default.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, /** @type {string} */ (remotePath), options);
return '';
}
/**
* Stops and removes all web socket handlers that are listening
* in scope of the currect session.
*
* @param {Object} server - The instance of NodeJs HTTP server,
* which hosts Appium
* @param {string|null} sessionId - The id of the current session
*/
async function removeAllSessionWebSocketHandlers(server, sessionId) {
if (!server || !lodash_1.default.isFunction(server.getWebSocketHandlers)) {
return;
}
const activeHandlers = await server.getWebSocketHandlers(sessionId);
for (const pathname of lodash_1.default.keys(activeHandlers)) {
await server.removeWebSocketHandler(pathname);
}
}
/**
* Returns true if the urlString is localhost
* @param {string} urlString
* @returns {boolean} Return true if the urlString is localhost
*/
function isLocalHost(urlString) {
try {
const hostname = /** @type {string} */ (node_url_1.default.parse(urlString).hostname);
return ['localhost', '127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(hostname);
}
catch {
logger_1.default.warn(`'${urlString}' cannot be parsed as a valid URL`);
}
return false;
}
/**
* Normalizes platformVersion to a valid iOS version string
*
* @param {string} originalVersion - Loose version number, that can be parsed by semver
* @return {string} iOS version number in <major>.<minor> format
* @throws {Error} 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 {string|Array<string>} argNames one or more key names
* @param {Object} opts the object to check
* @returns {Object} 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 {string} action - Description of action
* @param {import('./driver').XCUITestDriver} driver
* @returns {Simulator}
*/
function assertSimulator(action, driver) {
if (!driver.isSimulator()) {
throw new Error(`${lodash_1.default.upperFirst(action)} can only be performed on Simulator`);
}
return /** @type{Simulator} */ (driver.device);
}
/**
* Check if platform name is the TV OS one.
*
* @param {string|null|undefined} platformName
* @returns {boolean}
*/
function isTvOs(platformName) {
return lodash_1.default.toLower(platformName ?? '') === lodash_1.default.toLower(desired_caps_1.PLATFORM_NAME_TVOS);
}
/**
* Return normalized platform name.
*
* @param {string|null|undefined} platformName
* @returns {string}
*/
function normalizePlatformName(platformName) {
return isTvOs(platformName) ? desired_caps_1.PLATFORM_NAME_TVOS : desired_caps_1.PLATFORM_NAME_IOS;
}
/**
* @param {import('./driver').XCUITestDriverOpts} opts
* @returns {boolean}
*/
function shouldSetInitialSafariUrl(opts) {
return !(opts.safariInitialUrl === '' || (opts.noReset && lodash_1.default.isNil(opts.safariInitialUrl)))
&& !opts.initialDeeplinkUrl;
}
/**
* @param {import('./driver').XCUITestDriverOpts} opts
* @returns {boolean}
*/
function isIos17OrNewer(opts) {
return !!opts.platformVersion && support_1.util.compareVersions(opts.platformVersion, '>=', '17.0');
}
/**
* @typedef {import('appium-xcode').XcodeVersion} XcodeVersion
* @typedef {import('appium-ios-simulator').Simulator} Simulator
*/
//# sourceMappingURL=utils.js.map