UNPKG

appium-xcuitest-driver

Version:

Appium driver for iOS using XCUITest for backend

272 lines (254 loc) 9.55 kB
import _ from 'lodash'; import {errors} from 'appium/driver'; import moment from 'moment-timezone'; import {utilities} from 'appium-ios-device'; import {exec} from 'teen_process'; const MOMENT_FORMAT_ISO8601 = 'YYYY-MM-DDTHH:mm:ssZ'; const commands = { /** * @this {XCUITestDriver} */ async active() { if (this.isWebContext()) { return this.cacheWebElements(await this.executeAtom('active_element', [])); } return await this.proxyCommand(`/element/active`, 'GET'); }, /** * Trigger a touch/fingerprint match or match failure * * @param {boolean} match - whether the match should be a success or failure * @this {XCUITestDriver} */ async touchId(match = true) { await this.mobileSendBiometricMatch('touchId', match); }, /** * Toggle whether the device is enrolled in the touch ID program * * @param {boolean} isEnabled - whether to enable or disable the touch ID program * * @this {XCUITestDriver} */ async toggleEnrollTouchId(isEnabled = true) { await this.mobileEnrollBiometric(isEnabled); }, /** * Get the window size * @this {XCUITestDriver} * @returns {Promise<import('@appium/types').Size>} */ async getWindowSize() { const {width, height} = await this.getWindowRect(); return {width, height}; }, /** * Retrieves the actual device time. * * @param {string} format - The format specifier string. Read the [MomentJS documentation](https://momentjs.com/docs/) to get the full list of supported datetime format specifiers. The default format is `YYYY-MM-DDTHH:mm:ssZ`, which complies to ISO-8601. * @returns {Promise<string>} Formatted datetime string or the raw command output (if formatting fails) * @this {XCUITestDriver} */ async getDeviceTime(format = MOMENT_FORMAT_ISO8601) { this.log.info('Attempting to capture iOS device date and time'); if (!this.isRealDevice()) { this.log.info('On simulator. Assuming device time is the same as host time'); const cmd = 'date'; const args = ['+%Y-%m-%dT%H:%M:%S%z']; const inputFormat = 'YYYY-MM-DDTHH:mm:ssZZ'; const stdout = (await exec(cmd, args)).stdout.trim(); this.log.debug(`Got the following output out of '${cmd} ${args.join(' ')}': ${stdout}`); const parsedTimestamp = moment.utc(stdout, inputFormat); if (!parsedTimestamp.isValid()) { this.log.warn( `Cannot parse the timestamp '${stdout}' returned by '${cmd}' command. Returning it as is`, ); return stdout; } // @ts-expect-error This internal prop of moment is evidently a private API return parsedTimestamp.utcOffset(parsedTimestamp._tzm || 0).format(format); } const {timestamp, utcOffset, timeZone} = await utilities.getDeviceTime(this.opts.udid); this.log.debug(`timestamp: ${timestamp}, utcOffset: ${utcOffset}, timeZone: ${timeZone}`); const utc = moment.unix(timestamp).utc(); // at some point of time Apple started to return timestamps // in utcOffset instead of actual UTC offsets if (Math.abs(utcOffset) <= 12 * 60) { return utc.utcOffset(utcOffset).format(format); } // timeZone could either be a time zone name or // an UTC offset in seconds if (_.includes(timeZone, '/')) { return utc.tz(timeZone).format(format); } if (Math.abs(timeZone) <= 12 * 60 * 60) { return utc.utcOffset(timeZone / 60).format(format); } this.log.warn('Did not know how to apply the UTC offset. Returning the timestamp without it'); return utc.format(format); }, /** * Retrieves the current device time * * @param {string} format - See {@linkcode getDeviceTime.format} * @returns {Promise<string>} Formatted datetime string or the raw command output if formatting fails * @this {XCUITestDriver} */ async mobileGetDeviceTime(format = MOMENT_FORMAT_ISO8601) { return await this.getDeviceTime(format); }, /** * For W3C * @this {XCUITestDriver} * @return {Promise<import('@appium/types').Rect>} */ async getWindowRect() { if (this.isWebContext()) { const script = 'return {' + 'x: window.screenX || 0,' + 'y: window.screenY || 0,' + 'width: window.innerWidth,' + 'height: window.innerHeight' + '}'; return await this.executeAtom('execute_script', [script]); } return /** @type {import('@appium/types').Rect} */ ( await this.proxyCommand('/window/rect', 'GET') ); }, /** * @this {XCUITestDriver} */ async removeApp(bundleId) { return await this.mobileRemoveApp(bundleId); }, /** * @this {XCUITestDriver} */ async launchApp() { throw new Error( `The launchApp API has been deprecated and is not supported anymore. ` + `Consider using corresponding 'mobile:' extensions to manage the state of the app under test.`, ); }, /** * @this {XCUITestDriver} */ async closeApp() { throw new Error( `The closeApp API has been deprecated and is not supported anymore. ` + `Consider using corresponding 'mobile:' extensions to manage the state of the app under test.`, ); }, /** * @this {XCUITestDriver} */ async setUrl(url) { this.log.debug(`Attempting to set url '${url}'`); if (this.isWebContext()) { this.setCurrentUrl(url); // make sure to clear out any leftover web frames this.curWebFrames = []; await (/** @type {import('appium-remote-debugger').RemoteDebugger} */ (this.remote)).navToUrl(url); return; } if (this.isRealDevice()) { await this.proxyCommand('/url', 'POST', {url}); } else { await /** @type {import('../driver').Simulator} */ (this.device).simctl.openUrl(url); } }, /** * Retrieves the viewport dimensions. * * The viewport is the device's screen size with status bar size subtracted if the latter is present/visible. * @returns {Promise<import('./types').Viewport>} * @this {XCUITestDriver} */ async getViewportRect() { const scale = await this.getDevicePixelRatio(); // status bar height comes in unscaled, so scale it const statusBarHeight = Math.round((await this.getStatusBarHeight()) * scale); const windowSize = await this.getWindowRect(); // ios returns coordinates/dimensions in logical pixels, not device pixels, // so scale up to device pixels. status bar height is already scaled. return { left: 0, top: statusBarHeight, width: windowSize.width * scale, height: windowSize.height * scale - statusBarHeight, }; }, /** * Get information about the screen. * * @privateRemarks memoized in constructor * @this {XCUITestDriver} * @returns {Promise<ScreenInfo>} */ async getScreenInfo() { return /** @type {ScreenInfo} */ (await this.proxyCommand('/wda/screen', 'GET')); }, /** * @this {XCUITestDriver} */ async getStatusBarHeight() { const {statusBarSize} = await this.getScreenInfo(); return statusBarSize.height; }, /** * memoized in constructor * @this {XCUITestDriver} */ async getDevicePixelRatio() { const {scale} = await this.getScreenInfo(); return scale; }, /** * Emulates press action on the given physical device button. * * This executes different methods based on the platform: * * - iOS: [`pressButton:`](https://developer.apple.com/documentation/xctest/xcuidevice/1619052-pressbutton) * - tvOS: [`pressButton:`](https://developer.apple.com/documentation/xctest/xcuiremote/1627475-pressbutton) or [`pressButton:forDuration:`](https://developer.apple.com/documentation/xctest/xcuiremote/1627476-pressbutton) * * Use {@linkcode mobilePerformIoHidEvent} to call a more universal API to perform a button press with duration on any supported device. * * @param {import('./types').ButtonName} name - The name of the button to be pressed. * @param {number} [durationSeconds] - The duration of the button press in seconds (float). * @this {XCUITestDriver} */ async mobilePressButton(name, durationSeconds) { if (!name) { throw new errors.InvalidArgumentError('Button name is mandatory'); } if (!_.isNil(durationSeconds) && !_.isNumber(durationSeconds)) { throw new errors.InvalidArgumentError('durationSeconds should be a number'); } return await this.proxyCommand('/wda/pressButton', 'POST', {name, duration: durationSeconds}); }, /** * Process a string as speech and send it to Siri. * * Presents the Siri UI, if it is not currently active, and accepts a string which is then processed as if it were recognized speech. See [the documentation of `activateWithVoiceRecognitionText`](https://developer.apple.com/documentation/xctest/xcuisiriservice/2852140-activatewithvoicerecognitiontext?language=objc) for more details. * @param {string} text - Text to be sent to Siri * @returns {Promise<void>} * @this {XCUITestDriver} */ async mobileSiriCommand(text) { if (!text) { throw new errors.InvalidArgumentError('"text" argument is mandatory'); } await this.proxyCommand('/wda/siri/activate', 'POST', {text}); }, }; export default commands; /** * @typedef {Object} PressButtonOptions * @property {string} name - The name of the button to be pressed. * @property {number} [durationSeconds] - Duration in float seconds. */ /** * @typedef {import('../driver').XCUITestDriver} XCUITestDriver * @typedef {import('./types').ScreenInfo} ScreenInfo */