UNPKG

appium-xcuitest-driver

Version:

Appium driver for iOS using XCUITest for backend

471 lines 24 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.parseContainerPath = parseContainerPath; const lodash_1 = __importDefault(require("lodash")); const support_1 = require("appium/support"); const path_1 = __importDefault(require("path")); const appium_ios_device_1 = require("appium-ios-device"); const ios_fs_helpers_1 = require("../ios-fs-helpers"); const driver_1 = require("appium/driver"); const CONTAINER_PATH_MARKER = '@'; // https://regex101.com/r/PLdB0G/2 const CONTAINER_PATH_PATTERN = new RegExp(`^${CONTAINER_PATH_MARKER}([^/]+)/(.*)`); const CONTAINER_TYPE_SEPARATOR = ':'; const CONTAINER_DOCUMENTS_PATH = 'Documents'; const OBJECT_NOT_FOUND_ERROR_MESSAGE = 'OBJECT_NOT_FOUND'; /** * Parses the actual path and the bundle identifier from the given path string * * @this {XCUITestDriver} * @param {string} remotePath - The given path string. The string should * match `CONTAINER_PATH_PATTERN` regexp, otherwise an error is going * to be thrown. A valid string example: `@bundle.identifier:container_type/relative_path_in_container` * @param {import('./types').ContainerRootSupplier|string} [containerRootSupplier] - Container root path supplier function or string value * @returns {Promise<import('./types').ContainerObject>} */ async function parseContainerPath(remotePath, containerRootSupplier) { const match = CONTAINER_PATH_PATTERN.exec(remotePath); if (!match) { throw new Error(`It is expected that package identifier ` + `starts with '${CONTAINER_PATH_MARKER}' and is separated from the ` + `relative path with a single slash. '${remotePath}' is given instead`); } let [, bundleId, relativePath] = match; let containerType = null; const typeSeparatorPos = bundleId.indexOf(CONTAINER_TYPE_SEPARATOR); // We only consider container type exists if its length is greater than zero // not counting the colon if (typeSeparatorPos > 0 && typeSeparatorPos < bundleId.length - 1) { containerType = bundleId.substring(typeSeparatorPos + 1); this.log.debug(`Parsed container type: ${containerType}`); bundleId = bundleId.substring(0, typeSeparatorPos); } if (lodash_1.default.isNil(containerRootSupplier)) { const pathInContainer = relativePath; return { bundleId, pathInContainer, containerType }; } const containerRoot = lodash_1.default.isFunction(containerRootSupplier) ? await containerRootSupplier(bundleId, containerType) : containerRootSupplier; const pathInContainer = path_1.default.posix.resolve(containerRoot, relativePath); verifyIsSubPath(pathInContainer, containerRoot); return { bundleId, pathInContainer, containerType }; } /** * * @param {string} originalPath * @param {string} root * @returns {void} */ function verifyIsSubPath(originalPath, root) { const normalizedRoot = path_1.default.normalize(root); const normalizedPath = path_1.default.normalize(path_1.default.dirname(originalPath)); // If originalPath is root, `/`, originalPath should equal to normalizedRoot if (normalizedRoot !== originalPath && !normalizedPath.startsWith(normalizedRoot)) { throw new Error(`'${normalizedPath}' is expected to be a subpath of '${normalizedRoot}'`); } } /** * * @this {XCUITestDriver} * @param {string} [bundleId] * @param {string} [containerType] * @returns {Promise<any>} */ async function createAfcClient(bundleId, containerType) { const udid = this.device.udid; if (!bundleId) { return await appium_ios_device_1.services.startAfcService(udid); } const service = await appium_ios_device_1.services.startHouseArrestService(udid); const { skipDocumentsContainerCheck = false, } = await this.settings.getSettings(); if (skipDocumentsContainerCheck) { return service.vendContainer(bundleId); } return isDocumentsContainer(containerType) ? await service.vendDocuments(bundleId) : await service.vendContainer(bundleId); } /** * * @param {string} [containerType] * @returns {boolean} */ function isDocumentsContainer(containerType) { return lodash_1.default.toLower(containerType) === lodash_1.default.toLower(CONTAINER_DOCUMENTS_PATH); } /** * * @this {XCUITestDriver} * @param {string} remotePath * @returns {Promise<{service: any, relativePath: string}>} */ async function createService(remotePath) { if (CONTAINER_PATH_PATTERN.test(remotePath)) { const { bundleId, pathInContainer, containerType } = await parseContainerPath.bind(this)(remotePath); const service = await createAfcClient.bind(this)(bundleId, containerType); const relativePath = isDocumentsContainer(containerType) ? path_1.default.join(CONTAINER_DOCUMENTS_PATH, pathInContainer) : pathInContainer; return { service, relativePath }; } else { const service = await createAfcClient.bind(this)(); return { service, relativePath: remotePath }; } } /** * Save the given base64 data chunk as a binary file on the Simulator under test. * * @this {XCUITestDriver} * @param {string} remotePath - The remote path on the device. This variable can be prefixed with * bundle id, so then the file will be uploaded to the corresponding * application container instead of the default media folder, for example * '@com.myapp.bla:data/RelativePathInContainer/111.png'. The '@' character at the * beginning of the argument is mandatory in such case. The colon at the end of bundle identifier * is optional and is used to distinguish the container type. * Possible values there are 'app', 'data', 'groups', '<A specific App Group container>'. * The default value is 'app'. * The relative folder path is ignored if the file is going to be uploaded * to the default media folder and only the file name is considered important. * @param {string} base64Data - Base-64 encoded content of the file to be uploaded. */ async function pushFileToSimulator(remotePath, base64Data) { const buffer = Buffer.from(base64Data, 'base64'); const device = /** @type {import('../driver').Simulator} */ (this.device); if (CONTAINER_PATH_PATTERN.test(remotePath)) { const { bundleId, pathInContainer: dstPath } = await parseContainerPath.bind(this)(remotePath, async (appBundle, containerType) => await device.simctl.getAppContainer(appBundle, containerType)); this.log.info(`Parsed bundle identifier '${bundleId}' from '${remotePath}'. ` + `Will put the data into '${dstPath}'`); if (!(await support_1.fs.exists(path_1.default.dirname(dstPath)))) { this.log.debug(`The destination folder '${path_1.default.dirname(dstPath)}' does not exist. Creating...`); await (0, support_1.mkdirp)(path_1.default.dirname(dstPath)); } await support_1.fs.writeFile(dstPath, buffer); return; } const dstFolder = await support_1.tempDir.openDir(); const dstPath = path_1.default.resolve(dstFolder, path_1.default.basename(remotePath)); try { await support_1.fs.writeFile(dstPath, buffer); await device.simctl.addMedia(dstPath); } finally { await support_1.fs.rimraf(dstFolder); } } /** * Save the given base64 data chunk as a binary file on the device under test. * * @this {XCUITestDriver} * @param {string} remotePath - The remote path on the device. This variable can be prefixed with * bundle id, so then the file will be uploaded to the corresponding * application container instead of the default media folder. Use * `@<app_bundle_id>:<optional_container_type>/<path_to_the_file_or_folder_inside_container>` * format to pull a file or a folder from an application container of the given type. * The only supported container type is 'documents'. If the container type is not set * explicitly for a bundle id, then the default application container is going to be mounted * (aka --container ifuse argument) * e.g. If `@com.myapp.bla:documents/111.png` is provided, * `On My iPhone/<app name>` in Files app will be mounted in the host machine. * Base64 encoded `111.png` will be pushed into `On My iPhone/<app name>/111.png` * as base64 decoded data. * @param {string} base64Data - Base-64 encoded content of the file to be uploaded. */ async function pushFileToRealDevice(remotePath, base64Data) { const { service, relativePath } = await createService.bind(this)(remotePath); try { await (0, ios_fs_helpers_1.pushFile)(service, Buffer.from(base64Data, 'base64'), relativePath); } catch (e) { this.log.debug(e.stack); throw new Error(`Could not push the file to '${remotePath}'. Original error: ${e.message}`); } finally { service.close(); } } /** * * @this {XCUITestDriver} * @param {string} remotePath * @returns {Promise<void>} */ async function deleteFileOrFolder(remotePath) { return this.isSimulator() ? await deleteFromSimulator.bind(this)(remotePath) : await deleteFromRealDevice.bind(this)(remotePath); } /** * Get the content of given file or folder from iOS Simulator and return it as base-64 encoded string. * Folder content is recursively packed into a zip archive. * * @this {XCUITestDriver} * @param {string} remotePath - The path to a file or a folder, which exists in the corresponding application * container on Simulator. Use * `@<app_bundle_id>:<optional_container_type>/<path_to_the_file_or_folder_inside_container>` * format to pull a file or a folder from an application container of the given type. * Possible container types are `app`, `data`, `groups`, `<A specific App Group container>`. * The default type is `app`. * @param {boolean} isFile - Whether the destination item is a file or a folder * @returns {Promise<string>} Base-64 encoded content of the file. */ async function pullFromSimulator(remotePath, isFile) { let pathOnServer; const device = /** @type {import('../driver').Simulator} */ (this.device); if (CONTAINER_PATH_PATTERN.test(remotePath)) { const { bundleId, pathInContainer: dstPath } = await parseContainerPath.bind(this)(remotePath, async (appBundle, containerType) => await device.simctl.getAppContainer(appBundle, containerType)); this.log.info(`Parsed bundle identifier '${bundleId}' from '${remotePath}'. ` + `Will get the data from '${dstPath}'`); pathOnServer = dstPath; } else { const simRoot = device.getDir(); pathOnServer = path_1.default.posix.join(simRoot, remotePath); verifyIsSubPath(pathOnServer, simRoot); this.log.info(`Got the full item path: ${pathOnServer}`); } if (!(await support_1.fs.exists(pathOnServer))) { throw this.log.errorWithException(`The remote ${isFile ? 'file' : 'folder'} at '${pathOnServer}' does not exist`); } const buffer = isFile ? await support_1.util.toInMemoryBase64(pathOnServer) : await support_1.zip.toInMemoryZip(pathOnServer, { encodeToBase64: true }); return buffer.toString(); } /** * Get the content of given file or folder from the real device under test and return it as base-64 encoded string. * Folder content is recursively packed into a zip archive. * * @this {XCUITestDriver} * @param {string} remotePath - The path to an existing remote file on the device. This variable can be prefixed with * bundle id, so then the file will be downloaded from the corresponding * application container instead of the default media folder. Use * `@<app_bundle_id>:<optional_container_type>/<path_to_the_file_or_folder_inside_container>` * format to pull a file or a folder from an application container of the given type. * The only supported container type is 'documents'. If the container type is not set * explicitly for a bundle id, then the default application container is going to be mounted * (aka --container ifuse argument) * e.g. If `@com.myapp.bla:documents/111.png` is provided, * `On My iPhone/<app name>` in Files app will be mounted in the host machine. * `On My iPhone/<app name>/111.png` wil be pulled into the mounted host machine * and Appium returns the data as base64-encoded string to client. * `@com.myapp.bla:documents/` means `On My iPhone/<app name>`. * @param {boolean} isFile - Whether the destination item is a file or a folder * @returns {Promise<string>} Base-64 encoded content of the remote file */ async function pullFromRealDevice(remotePath, isFile) { const { service, relativePath } = await createService.bind(this)(remotePath); try { const fileInfo = await service.getFileInfo(relativePath); if (isFile && fileInfo.isDirectory()) { throw new Error(`The requested path is not a file. Path: '${remotePath}'`); } if (!isFile && !fileInfo.isDirectory()) { throw new Error(`The requested path is not a folder. Path: '${remotePath}'`); } return fileInfo.isFile() ? (await (0, ios_fs_helpers_1.pullFile)(service, relativePath)).toString('base64') : (await (0, ios_fs_helpers_1.pullFolder)(service, relativePath)).toString(); } finally { service.close(); } } /** * Remove the file or folder from the device * * @this {XCUITestDriver} * @param {string} remotePath - The path to a file or a folder, which exists in the corresponding application * container on Simulator. Use * `@<app_bundle_id>:<optional_container_type>/<path_to_the_file_or_folder_inside_container>` * format to pull a file or a folder from an application container of the given type. * Possible container types are 'app', 'data', 'groups', '<A specific App Group container>'. * The default type is 'app'. * @returns {Promise<void>} */ async function deleteFromSimulator(remotePath) { let pathOnServer; const device = /** @type {import('../driver').Simulator} */ (this.device); if (CONTAINER_PATH_PATTERN.test(remotePath)) { const { bundleId, pathInContainer: dstPath } = await parseContainerPath.bind(this)(remotePath, async (appBundle, containerType) => await device.simctl.getAppContainer(appBundle, containerType)); this.log.info(`Parsed bundle identifier '${bundleId}' from '${remotePath}'. ` + `'${dstPath}' will be deleted`); pathOnServer = dstPath; } else { const simRoot = device.getDir(); pathOnServer = path_1.default.posix.join(simRoot, remotePath); verifyIsSubPath(pathOnServer, simRoot); this.log.info(`Got the full path: ${pathOnServer}`); } if (!(await support_1.fs.exists(pathOnServer))) { throw new driver_1.errors.InvalidArgumentError(`The remote path at '${pathOnServer}' does not exist`); } await support_1.fs.rimraf(pathOnServer); } /** * Remove the file or folder from the device * * @this {XCUITestDriver} * @param {string} remotePath - The path to an existing remote file on the device. This variable can be prefixed with * bundle id, so then the file will be downloaded from the corresponding * application container instead of the default media folder. Use * `@<app_bundle_id>:<optional_container_type>/<path_to_the_file_or_folder_inside_container>` * format to pull a file or a folder from an application container of the given type. * The only supported container type is 'documents'. If the container type is not set * explicitly for a bundle id, then the default application container is going to be mounted * (aka --container ifuse argument) * e.g. If `@com.myapp.bla:documents/111.png` is provided, * `On My iPhone/<app name>` in Files app will be mounted in the host machine. * `On My iPhone/<app name>/111.png` wil be pulled into the mounted host machine * and Appium returns the data as base64-encoded string to client. * `@com.myapp.bla:documents/` means `On My iPhone/<app name>`. * @returns {Promise<void>} */ async function deleteFromRealDevice(remotePath) { const { service, relativePath } = await createService.bind(this)(remotePath); try { await service.deleteDirectory(relativePath); } catch (e) { if (e.message.includes(OBJECT_NOT_FOUND_ERROR_MESSAGE)) { throw new Error(`Path '${remotePath}' does not exist on the device`); } throw e; } finally { service.close(); } } exports.default = { /** * Pushes the given data to a file on the remote device * * @param {string} remotePath The full path to the remote file or * a file inside a package bundle. Check the documentation on * `pushFileToRealDevice` and `pushFileToSimulator` for more information * on acceptable values. * @param {string} base64Data Base64 encoded data to be written to the * remote file. The remote file will be silently overridden if it already exists. * @throws {Error} If there was an error while pushing the data * @this {XCUITestDriver} */ async pushFile(remotePath, base64Data) { if (remotePath.endsWith('/')) { throw new driver_1.errors.InvalidArgumentError(`It is expected that remote path points to a file and not to a folder. ` + `'${remotePath}' is given instead`); } if (lodash_1.default.isArray(base64Data)) { // some clients (ahem) java, send a byte array encoding utf8 characters // instead of a string, which would be infinitely better! base64Data = Buffer.from(base64Data).toString('utf8'); } return this.isSimulator() ? await pushFileToSimulator.bind(this)(remotePath, base64Data) : await pushFileToRealDevice.bind(this)(remotePath, base64Data); }, /** * Pushes the given data to a file on the remote device. * * @param {string} remotePath - The full path to the remote file * or a specially formatted path, which points to an item inside an app bundle. * @param {string} payload - Base64-encoded content of the file to be pushed. * @this {XCUITestDriver} */ async mobilePushFile(remotePath, payload) { return await this.pushFile(remotePath, payload); }, /** * Pulls a remote file from the device. * * @param {string} remotePath The full path to the remote file * or a specially formatted path, which points to an item inside app bundle. * See the documentation for `pullFromRealDevice` and `pullFromSimulator` * to get more information on acceptable values. * @returns {Promise<string>} Base64 encoded content of the pulled file * @throws {Error} If the pull operation failed * @this {XCUITestDriver} */ async pullFile(remotePath) { if (remotePath.endsWith('/')) { throw new driver_1.errors.InvalidArgumentError(`It is expected that remote path points to a file and not to a folder. ` + `'${remotePath}' is given instead`); } return this.isSimulator() ? await pullFromSimulator.bind(this)(remotePath, true) : await pullFromRealDevice.bind(this)(remotePath, true); }, /** * Pulls a remote file from the device. * * @param {string} remotePath - The full path to the remote file * or a specially formatted path, which points to an item inside app bundle. See the documentation for `pullFromRealDevice` and `pullFromSimulator` to get more information on acceptable values. * @returns {Promise<string>} The same as in `pullFile` * @this {XCUITestDriver} */ async mobilePullFile(remotePath) { return await this.pullFile(remotePath); }, /** * Delete a remote folder from the device. * * @param {string} remotePath - The full path to the remote folder or a specially formatted path, which points to an item inside app bundle. See the documentation for `pullFromRealDevice` and `pullFromSimulator` to get more information on acceptable values. * @this {XCUITestDriver} * @returns {Promise<void>} */ async mobileDeleteFolder(remotePath) { if (!remotePath.endsWith('/')) { remotePath = `${remotePath}/`; } await deleteFileOrFolder.bind(this)(remotePath); }, /** * Delete a remote file from the device. * * @param {string} remotePath - The full path to the remote file or a specially formatted path, which points to an item inside app bundle. See the documentation for `pullFromRealDevice` and `pullFromSimulator` to get more information on acceptable values. * @this {XCUITestDriver} * @returns {Promise<void>} */ async mobileDeleteFile(remotePath) { if (remotePath.endsWith('/')) { throw new driver_1.errors.InvalidArgumentError(`It is expected that remote path points to a file and not to a folder. ` + `'${remotePath}' is given instead`); } await deleteFileOrFolder.bind(this)(remotePath); }, /** * Pulls the whole folder from the remote device * * @param {string} remotePath The full path to a folder on the * remote device or a folder inside an application bundle * @returns {Promise<string>} Zipped and base64-encoded content of the folder * @throws {Error} If there was a failure while getting the folder content * @this {XCUITestDriver} */ async pullFolder(remotePath) { if (!remotePath.endsWith('/')) { remotePath = `${remotePath}/`; } return this.isSimulator() ? await pullFromSimulator.bind(this)(remotePath, false) : await pullFromRealDevice.bind(this)(remotePath, false); }, /** * Pulls the whole folder from the device under test. * * @param {string} remotePath - The full path to the remote folder * @returns {Promise<string>} The same as `pullFolder` * @this {XCUITestDriver} */ async mobilePullFolder(remotePath) { return await this.pullFolder(remotePath); }, }; /** * @typedef {import('../driver').XCUITestDriver} XCUITestDriver */ //# sourceMappingURL=file-movement.js.map