appium-xcuitest-driver
Version:
Appium driver for iOS using XCUITest for backend
446 lines • 21.5 kB
JavaScript
;
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 node_path_1 = __importDefault(require("node:path"));
const real_device_management_1 = require("../device/real-device-management");
const driver_1 = require("appium/driver");
const utils_1 = require("../utils");
const afc_client_1 = require("../device/afc-client");
//#endregion
//#region Constants
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';
//#endregion
//#region Public Exported Functions
/**
* 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) {
if (typeSeparatorPos < bundleId.length - 1) {
containerType = bundleId.substring(typeSeparatorPos + 1);
this.log.debug(`Parsed container type: ${containerType}`);
}
// Always strip the colon and everything after it
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 = node_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);
}
//#endregion
//#region Private Helper Functions
/**
* Delete file or folder helper
*/
async function deleteFileOrFolder(remotePath) {
return this.isSimulator()
? await deleteFromSimulator.bind(this)(remotePath)
: await deleteFromRealDevice.bind(this)(remotePath);
}
/**
* Check if container type refers to documents container
*/
function isDocumentsContainer(containerType) {
return lodash_1.default.toLower(containerType ?? '') === lodash_1.default.toLower(CONTAINER_DOCUMENTS_PATH);
}
/**
* Verify that a path is a subpath of a root directory
*/
function verifyIsSubPath(originalPath, root) {
const normalizedRoot = node_path_1.default.normalize(root);
const normalizedPath = node_path_1.default.normalize(node_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}'`);
}
}
/**
* Create AFC client for file operations
*/
async function createAfcClient(opts = {}) {
const { bundleId, containerType } = opts;
const udid = this.device.udid;
const useIos18 = (0, utils_1.isIos18OrNewer)(this.opts);
if (bundleId) {
const skipDocumentsCheck = this.settings.getSettings().skipDocumentsContainerCheck ?? false;
return await afc_client_1.AfcClient.createForApp(udid, bundleId, useIos18, {
containerType: containerType ?? null,
skipDocumentsCheck,
});
}
return await afc_client_1.AfcClient.createForDevice(udid, useIos18);
}
/**
* Create service for file operations
*/
async function createService(remotePath) {
if (CONTAINER_PATH_PATTERN.test(remotePath)) {
const { bundleId, pathInContainer, containerType } = await parseContainerPath.bind(this)(remotePath);
const client = await createAfcClient.bind(this)({ bundleId, containerType });
let relativePath = isDocumentsContainer(containerType)
? node_path_1.default.join(CONTAINER_DOCUMENTS_PATH, pathInContainer)
: pathInContainer;
// Ensure path starts with / for AFC operations
if (!relativePath.startsWith('/')) {
relativePath = `/${relativePath}`;
}
return { client, relativePath };
}
const client = await createAfcClient.bind(this)({});
return { client, 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(node_path_1.default.dirname(dstPath)))) {
this.log.debug(`The destination folder '${node_path_1.default.dirname(dstPath)}' does not exist. Creating...`);
await support_1.fs.mkdirp(node_path_1.default.dirname(dstPath));
}
await support_1.fs.writeFile(dstPath, buffer);
return;
}
const dstFolder = await support_1.tempDir.openDir();
const dstPath = node_path_1.default.resolve(dstFolder, node_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 { client, relativePath } = await createService.bind(this)(remotePath);
try {
await (0, real_device_management_1.pushFile)(client, 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}`, { cause: e });
}
finally {
await client.close();
}
}
/**
* 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 = node_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 { client, relativePath } = await createService.bind(this)(remotePath);
try {
// Check if path is a directory
const isDirectory = await client.isDirectory(relativePath);
if (isFile && isDirectory) {
throw new Error(`The requested path is not a file. Path: '${remotePath}'`);
}
if (!isFile && !isDirectory) {
throw new Error(`The requested path is not a folder. Path: '${remotePath}'`);
}
return isDirectory
? (await (0, real_device_management_1.pullFolder)(client, relativePath)).toString()
: (await (0, real_device_management_1.pullFile)(client, relativePath)).toString('base64');
}
finally {
await client.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 = node_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 { client, relativePath } = await createService.bind(this)(remotePath);
try {
await client.deleteDirectory(relativePath);
}
catch (e) {
if (e.message.includes(OBJECT_NOT_FOUND_ERROR_MESSAGE)) {
throw new Error(`Path '${remotePath}' does not exist on the device`, { cause: e });
}
throw e;
}
finally {
await client.close();
}
}
//#endregion
//# sourceMappingURL=file-movement.js.map