appium-xcuitest-driver
Version:
Appium driver for iOS using XCUITest for backend
254 lines (253 loc) • 10.7 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const lodash_1 = __importDefault(require("lodash"));
const driver_1 = require("appium/driver");
const moment_timezone_1 = __importDefault(require("moment-timezone"));
const appium_ios_device_1 = require("appium-ios-device");
const teen_process_1 = require("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 (0, teen_process_1.exec)(cmd, args)).stdout.trim();
this.log.debug(`Got the following output out of '${cmd} ${args.join(' ')}': ${stdout}`);
const parsedTimestamp = moment_timezone_1.default.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 appium_ios_device_1.utilities.getDeviceTime(this.opts.udid);
this.log.debug(`timestamp: ${timestamp}, utcOffset: ${utcOffset}, timeZone: ${timeZone}`);
const utc = moment_timezone_1.default.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 (lodash_1.default.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 driver_1.errors.InvalidArgumentError('Button name is mandatory');
}
if (!lodash_1.default.isNil(durationSeconds) && !lodash_1.default.isNumber(durationSeconds)) {
throw new driver_1.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 driver_1.errors.InvalidArgumentError('"text" argument is mandatory');
}
await this.proxyCommand('/wda/siri/activate', 'POST', { text });
},
};
exports.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
*/
//# sourceMappingURL=general.js.map