UNPKG

appium-xcuitest-driver

Version:

Appium driver for iOS using XCUITest for backend

418 lines 20.7 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; exports.pushFile = pushFile; exports.mobilePushFile = mobilePushFile; exports.pullFile = pullFile; exports.mobilePullFile = mobilePullFile; exports.mobileDeleteFolder = mobileDeleteFolder; exports.mobileDeleteFile = mobileDeleteFile; exports.pullFolder = pullFolder; exports.mobilePullFolder = mobilePullFolder; 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 real_device_management_1 = require("../device/real-device-management"); 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. * * @param remotePath - Path string matching `CONTAINER_PATH_PATTERN`, e.g. `@bundle.id:container/relative/path` * @param containerRootSupplier - Container root path supplier or explicit root */ 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`); } const [, bundleIdMatch, relativePath] = match; let bundleId = bundleIdMatch; 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 }; } /** * Pushes the given data to a file on the remote device. * * @param 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 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 */ async function 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`); } let b64StringData; 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! b64StringData = Buffer.from(base64Data).toString('utf8'); } else if (Buffer.isBuffer(base64Data)) { b64StringData = base64Data.toString('utf8'); } else { b64StringData = base64Data; } return this.isSimulator() ? await pushFileToSimulator.bind(this)(remotePath, b64StringData) : await pushFileToRealDevice.bind(this)(remotePath, b64StringData); } /** * Pushes the given data to a file on the remote device. * * @param remotePath - The full path to the remote file * or a specially formatted path, which points to an item inside an app bundle. * @param payload - Base64-encoded content of the file to be pushed. */ async function mobilePushFile(remotePath, payload) { return await this.pushFile(remotePath, payload); } /** * Pulls a remote file from the device. * * @param 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 Base64 encoded content of the pulled file * @throws {Error} If the pull operation failed */ async function 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 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 The same as in `pullFile` */ async function mobilePullFile(remotePath) { return await this.pullFile(remotePath); } /** * Delete a remote folder from the device. * * @param 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. * @returns Nothing */ async function mobileDeleteFolder(remotePath) { if (!remotePath.endsWith('/')) { remotePath = `${remotePath}/`; } await deleteFileOrFolder.bind(this)(remotePath); } /** * Delete a remote file from the device. * * @param 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 Nothing */ async function 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 remotePath The full path to a folder on the * remote device or a folder inside an application bundle * @returns Zipped and base64-encoded content of the folder * @throws {Error} If there was a failure while getting the folder content */ async function 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 remotePath - The full path to the remote folder * @returns The same as `pullFolder` */ async function mobilePullFolder(remotePath) { return await this.pullFolder(remotePath); } 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}'`); } } 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); } function isDocumentsContainer(containerType) { return lodash_1.default.toLower(containerType ?? '') === lodash_1.default.toLower(CONTAINER_DOCUMENTS_PATH); } 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. * * @param remotePath - Remote path on the simulator. Supports bundle-id-prefixed format * (e.g. `@com.myapp.bla:data/path/in/container/file.png`) to target * application containers; otherwise uploads to the default media folder. * @param base64Data - Base-64 encoded content of the file to be uploaded. */ async function pushFileToSimulator(remotePath, base64Data) { const buffer = Buffer.from(base64Data, 'base64'); const device = 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 a real device. * * @param remotePath - Remote path on the device. Supports the same bundle-id-prefixed * format as simulator uploads (e.g. `@com.myapp.bla:documents/file.png`) * to target application containers; otherwise defaults to media folder. * @param 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, real_device_management_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(); } } 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. * * @param 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 isFile - Whether the destination item is a file or a folder * @returns Base-64 encoded content of the file. */ async function pullFromSimulator(remotePath, isFile) { let pathOnServer; const device = 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. * * @param 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` will 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 isFile - Whether the destination item is a file or a folder * @returns 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, real_device_management_1.pullFile)(service, relativePath)).toString('base64') : (await (0, real_device_management_1.pullFolder)(service, relativePath)).toString(); } finally { service.close(); } } /** * Remove the file or folder from the device * * @param 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 Nothing */ async function deleteFromSimulator(remotePath) { let pathOnServer; const device = 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 * * @param 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` will 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 Nothing */ 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(); } } //# sourceMappingURL=file-movement.js.map