diff --git a/lib/commands/actions.js b/lib/commands/actions.js index 2a20f84b..a521dd38 100644 --- a/lib/commands/actions.js +++ b/lib/commands/actions.js @@ -1,9 +1,9 @@ import androidHelpers from '../android-helpers'; -import { fs, util, tempDir, imageUtil } from '@appium/support'; +import {fs, util, tempDir, imageUtil} from '@appium/support'; import path from 'path'; import B from 'bluebird'; -import { exec } from 'teen_process'; -import { requireArgs } from '../utils'; +import {exec} from 'teen_process'; +import {requireArgs} from '../utils'; const swipeStepsPerSec = 28; const dragStepsPerSec = 40; @@ -12,21 +12,21 @@ const commands = {}; const helpers = {}; const extensions = {}; -commands.keyevent = async function keyevent (keycode, metastate = null) { +commands.keyevent = async function keyevent(keycode, metastate = null) { // TODO deprecate keyevent; currently wd only implements keyevent this.log.warn('keyevent will be deprecated use pressKeyCode'); return await this.pressKeyCode(keycode, metastate); }; -commands.pressKeyCode = async function pressKeyCode (keycode, metastate = null) { +commands.pressKeyCode = async function pressKeyCode(keycode, metastate = null) { return await this.bootstrap.sendAction('pressKeyCode', {keycode, metastate}); }; -commands.longPressKeyCode = async function longPressKeyCode (keycode, metastate = null) { +commands.longPressKeyCode = async function longPressKeyCode(keycode, metastate = null) { return await this.bootstrap.sendAction('longPressKeyCode', {keycode, metastate}); }; -commands.getOrientation = async function getOrientation () { +commands.getOrientation = async function getOrientation() { let params = { naturalOrientation: !!this.opts.androidNaturalOrientation, }; @@ -34,7 +34,7 @@ commands.getOrientation = async function getOrientation () { return orientation.toUpperCase(); }; -commands.setOrientation = async function setOrientation (orientation) { +commands.setOrientation = async function setOrientation(orientation) { orientation = orientation.toUpperCase(); let params = { orientation, @@ -43,24 +43,23 @@ commands.setOrientation = async function setOrientation (orientation) { return await this.bootstrap.sendAction('orientation', params); }; -commands.fakeFlick = async function fakeFlick (xSpeed, ySpeed) { +commands.fakeFlick = async function fakeFlick(xSpeed, ySpeed) { return await this.bootstrap.sendAction('flick', {xSpeed, ySpeed}); }; -commands.fakeFlickElement = async function fakeFlickElement (elementId, xoffset, yoffset, speed) { +commands.fakeFlickElement = async function fakeFlickElement(elementId, xoffset, yoffset, speed) { let params = {xoffset, yoffset, speed, elementId}; return await this.bootstrap.sendAction('element:flick', params); }; -commands.swipe = async function swipe (startX, startY, endX, endY, duration, touchCount, elId) { +commands.swipe = async function swipe(startX, startY, endX, endY, duration, touchCount, elId) { if (startX === 'null') { startX = 0.5; } if (startY === 'null') { startY = 0.5; } - let swipeOpts = {startX, startY, endX, endY, - steps: Math.round(duration * swipeStepsPerSec)}; + let swipeOpts = {startX, startY, endX, endY, steps: Math.round(duration * swipeStepsPerSec)}; // going the long way and checking for undefined and null since // we can't be assured `elId` is a string and not an int if (util.hasValue(elId)) { @@ -69,7 +68,7 @@ commands.swipe = async function swipe (startX, startY, endX, endY, duration, tou return await this.doSwipe(swipeOpts); }; -commands.doSwipe = async function doSwipe (swipeOpts) { +commands.doSwipe = async function doSwipe(swipeOpts) { if (util.hasValue(swipeOpts.elementId)) { return await this.bootstrap.sendAction('element:swipe', swipeOpts); } else { @@ -77,22 +76,40 @@ commands.doSwipe = async function doSwipe (swipeOpts) { } }; -commands.pinchClose = async function pinchClose (startX, startY, endX, endY, duration, percent, steps, elId) { +commands.pinchClose = async function pinchClose( + startX, + startY, + endX, + endY, + duration, + percent, + steps, + elId +) { let pinchOpts = { direction: 'in', elementId: elId, percent, - steps + steps, }; return await this.bootstrap.sendAction('element:pinch', pinchOpts); }; -commands.pinchOpen = async function pinchOpen (startX, startY, endX, endY, duration, percent, steps, elId) { +commands.pinchOpen = async function pinchOpen( + startX, + startY, + endX, + endY, + duration, + percent, + steps, + elId +) { let pinchOpts = {direction: 'out', elementId: elId, percent, steps}; return await this.bootstrap.sendAction('element:pinch', pinchOpts); }; -commands.flick = async function flick (element, xSpeed, ySpeed, xOffset, yOffset, speed) { +commands.flick = async function flick(element, xSpeed, ySpeed, xOffset, yOffset, speed) { if (element) { await this.fakeFlickElement(element, xOffset, yOffset, speed); } else { @@ -100,16 +117,29 @@ commands.flick = async function flick (element, xSpeed, ySpeed, xOffset, yOffset } }; -commands.drag = async function drag (startX, startY, endX, endY, duration, touchCount, elementId, destElId) { +commands.drag = async function drag( + startX, + startY, + endX, + endY, + duration, + touchCount, + elementId, + destElId +) { let dragOpts = { - elementId, destElId, startX, startY, endX, endY, - steps: Math.round(duration * dragStepsPerSec) + elementId, + destElId, + startX, + startY, + endX, + endY, + steps: Math.round(duration * dragStepsPerSec), }; return await this.doDrag(dragOpts); - }; -commands.doDrag = async function doDrag (dragOpts) { +commands.doDrag = async function doDrag(dragOpts) { if (util.hasValue(dragOpts.elementId)) { return await this.bootstrap.sendAction('element:drag', dragOpts); } else { @@ -117,7 +147,25 @@ commands.doDrag = async function doDrag (dragOpts) { } }; -commands.lock = async function lock (seconds) { +/** + * @typedef {Object} LockOptions + * @property {number?} seconds The number to keep the locked. + * 0 or empty value will keep the device locked. + */ + +/** + * Lock the device (and optionally unlock it after a certain amount of time). + + * @param {LockOptions} opts + * @throws {Error} if lock or unlock operation fails + */ + +commands.mobileLock = async function mobileLock(opts = {}) { + const {seconds} = opts; + return await this.lock(seconds); +}; + +commands.lock = async function lock(seconds) { await this.adb.lock(); if (isNaN(seconds)) { return; @@ -131,23 +179,23 @@ commands.lock = async function lock (seconds) { await this.unlock(); }; -commands.isLocked = async function isLocked () { +commands.isLocked = async function isLocked() { return await this.adb.isScreenLocked(); }; -commands.unlock = async function unlock () { +commands.unlock = async function unlock() { return await androidHelpers.unlock(this, this.adb, this.caps); }; -commands.openNotifications = async function openNotifications () { +commands.openNotifications = async function openNotifications() { return await this.bootstrap.sendAction('openNotification'); }; -commands.setLocation = async function setLocation (latitude, longitude) { +commands.setLocation = async function setLocation(latitude, longitude) { return await this.adb.sendTelnetCommand(`geo fix ${longitude} ${latitude}`); }; -commands.fingerprint = async function fingerprint (fingerprintId) { +commands.fingerprint = async function fingerprint(fingerprintId) { if (!this.isEmulator()) { this.log.errorAndThrow('fingerprint method is only available for emulators'); } @@ -174,7 +222,7 @@ commands.mobileFingerprint = async function mobileFingerprint(opts = {}) { await this.fingerprint(fingerprintId); }; -commands.sendSMS = async function sendSMS (phoneNumber, message) { +commands.sendSMS = async function sendSMS(phoneNumber, message) { if (!this.isEmulator()) { this.log.errorAndThrow('sendSMS method is only available for emulators'); } @@ -198,7 +246,7 @@ commands.mobileSendSms = async function mobileSendSms(opts = {}) { await this.sendSMS(phoneNumber, message); }; -commands.gsmCall = async function gsmCall (phoneNumber, action) { +commands.gsmCall = async function gsmCall(phoneNumber, action) { if (!this.isEmulator()) { this.log.errorAndThrow('gsmCall method is only available for emulators'); } @@ -222,7 +270,7 @@ commands.mobileGsmCall = async function mobileGsmCall(opts = {}) { await this.gsmCall(phoneNumber, action); }; -commands.gsmSignal = async function gsmSignal (signalStrengh) { +commands.gsmSignal = async function gsmSignal(signalStrengh) { if (!this.isEmulator()) { this.log.errorAndThrow('gsmSignal method is only available for emulators'); } @@ -245,7 +293,7 @@ commands.mobileGsmSignal = async function mobileGsmSignal(opts = {}) { await this.gsmSignal(strength); }; -commands.gsmVoice = async function gsmVoice (state) { +commands.gsmVoice = async function gsmVoice(state) { if (!this.isEmulator()) { this.log.errorAndThrow('gsmVoice method is only available for emulators'); } @@ -268,7 +316,7 @@ commands.mobileGsmVoice = async function mobileGsmVoice(opts = {}) { await this.gsmVoice(state); }; -commands.powerAC = async function powerAC (state) { +commands.powerAC = async function powerAC(state) { if (!this.isEmulator()) { this.log.errorAndThrow('powerAC method is only available for emulators'); } @@ -291,7 +339,7 @@ commands.mobilePowerAc = async function mobilePowerAc(opts = {}) { await this.powerAC(state); }; -commands.powerCapacity = async function powerCapacity (batteryPercent) { +commands.powerCapacity = async function powerCapacity(batteryPercent) { if (!this.isEmulator()) { this.log.errorAndThrow('powerCapacity method is only available for emulators'); } @@ -314,7 +362,7 @@ commands.mobilePowerCapacity = async function mobilePowerCapacity(opts = {}) { await this.powerCapacity(percent); }; -commands.networkSpeed = async function networkSpeed (networkSpeed) { +commands.networkSpeed = async function networkSpeed(networkSpeed) { if (!this.isEmulator()) { this.log.errorAndThrow('networkSpeed method is only available for emulators'); } @@ -349,7 +397,7 @@ commands.mobileNetworkSpeed = async function mobileNetworkSpeed(opts = {}) { * @throws {Error} - If value for the se sor is not defined * @throws {Error} - If deviceType is not an emulator */ -commands.sensorSet = async function sensorSet (sensor = {}) { +commands.sensorSet = async function sensorSet(sensor = {}) { const {sensorType, value} = sensor; if (!util.hasValue(sensorType)) { this.log.errorAndThrow(`'sensorType' argument is required`); @@ -368,7 +416,7 @@ commands.sensorSet = async function sensorSet (sensor = {}) { * @param {Object} opts * @returns {Promise} */ -helpers.getScreenshotDataWithAdbShell = async function getScreenshotDataWithAdbShell (adb, opts) { +helpers.getScreenshotDataWithAdbShell = async function getScreenshotDataWithAdbShell(adb, opts) { const localFile = await tempDir.path({prefix: 'appium', suffix: '.png'}); if (await fs.exists(localFile)) { await fs.unlink(localFile); @@ -377,7 +425,7 @@ helpers.getScreenshotDataWithAdbShell = async function getScreenshotDataWithAdbS const pngDir = opts.androidScreenshotPath || '/data/local/tmp/'; const png = path.posix.resolve(pngDir, 'screenshot.png'); await adb.shell(['/system/bin/rm', `${png};`, '/system/bin/screencap', '-p', png]); - if (!await adb.fileSize(png)) { + if (!(await adb.fileSize(png))) { throw new Error('The size of the taken screenshot equals to zero.'); } await adb.pull(png, localFile); @@ -393,7 +441,7 @@ helpers.getScreenshotDataWithAdbShell = async function getScreenshotDataWithAdbS * @param {ADB} adb * @returns {Promise} */ -helpers.getScreenshotDataWithAdbExecOut = async function getScreenshotDataWithAdbExecOut (adb) { +helpers.getScreenshotDataWithAdbExecOut = async function getScreenshotDataWithAdbExecOut(adb) { const {stdout, stderr, code} = await exec( adb.executable.path, [...adb.executable.defaultArgs, 'exec-out', '/system/bin/screencap', '-p'], @@ -411,7 +459,7 @@ helpers.getScreenshotDataWithAdbExecOut = async function getScreenshotDataWithAd return stdout; }; -commands.getScreenshot = async function getScreenshot () { +commands.getScreenshot = async function getScreenshot() { const apiLevel = await this.adb.getApiLevel(); let image = null; if (apiLevel > 20) { @@ -420,15 +468,18 @@ commands.getScreenshot = async function getScreenshot () { // to be executed. Unfortunately, exec-out option is only supported by newer Android/SDK versions (5.0 and later) image = await this.getScreenshotDataWithAdbExecOut(this.adb); } catch (e) { - this.log.info(`Cannot get screenshot data with 'adb exec-out' because of '${e.message}'. ` + - `Defaulting to 'adb shell' call`); + this.log.info( + `Cannot get screenshot data with 'adb exec-out' because of '${e.message}'. ` + + `Defaulting to 'adb shell' call` + ); } } if (!image) { try { image = await this.getScreenshotDataWithAdbShell(this.adb, this.opts); } catch (e) { - const err = `Cannot get screenshot data because of '${e.message}'. ` + + const err = + `Cannot get screenshot data because of '${e.message}'. ` + `Make sure the 'LayoutParams.FLAG_SECURE' is not set for ` + `the current view`; this.log.errorAndThrow(err); @@ -438,7 +489,10 @@ commands.getScreenshot = async function getScreenshot () { // Android bug 8433742 - rotate screenshot if screen is rotated let screenOrientation = await this.adb.getScreenOrientation(); try { - image = await imageUtil.requireSharp()(image).rotate(-90 * screenOrientation).toBuffer(); + image = await imageUtil + .requireSharp()(image) + .rotate(-90 * screenOrientation) + .toBuffer(); } catch (err) { this.log.warn(`Could not rotate screenshot due to error: ${err}`); } @@ -447,5 +501,5 @@ commands.getScreenshot = async function getScreenshot () { }; Object.assign(extensions, commands, helpers); -export { commands, helpers }; +export {commands, helpers}; export default extensions; diff --git a/lib/commands/execute.js b/lib/commands/execute.js index 730cfe88..b7171330 100644 --- a/lib/commands/execute.js +++ b/lib/commands/execute.js @@ -1,9 +1,9 @@ import _ from 'lodash'; -import { errors, PROTOCOLS } from 'appium/driver'; +import {errors, PROTOCOLS} from 'appium/driver'; const extensions = {}; -extensions.execute = async function execute (script, args) { +extensions.execute = async function execute(script, args) { if (script.match(/^mobile:/)) { this.log.info(`Executing native command '${script}'`); script = script.replace(/^mobile:/, '').trim(); @@ -12,16 +12,17 @@ extensions.execute = async function execute (script, args) { if (!this.isWebContext()) { throw new errors.NotImplementedError(); } - const endpoint = this.chromedriver.jwproxy.downstreamProtocol === PROTOCOLS.MJSONWP - ? '/execute' - : '/execute/sync'; + const endpoint = + this.chromedriver.jwproxy.downstreamProtocol === PROTOCOLS.MJSONWP + ? '/execute' + : '/execute/sync'; return await this.chromedriver.jwproxy.command(endpoint, 'POST', { script, args, }); }; -extensions.executeMobile = async function executeMobile (mobileCommand, opts = {}) { +extensions.executeMobile = async function executeMobile(mobileCommand, opts = {}) { const mobileCommandsMapping = { shell: 'mobileShell', @@ -66,6 +67,7 @@ extensions.executeMobile = async function executeMobile (mobileCommand, opts = { getContexts: 'mobileGetContexts', + lock: 'mobileLock', unlock: 'mobileUnlock', refreshGpsCache: 'mobileRefreshGpsCache', @@ -82,8 +84,10 @@ extensions.executeMobile = async function executeMobile (mobileCommand, opts = { }; if (!_.has(mobileCommandsMapping, mobileCommand)) { - throw new errors.UnknownCommandError(`Unknown mobile command "${mobileCommand}". ` + - `Only ${_.keys(mobileCommandsMapping)} commands are supported.`); + throw new errors.UnknownCommandError( + `Unknown mobile command "${mobileCommand}". ` + + `Only ${_.keys(mobileCommandsMapping)} commands are supported.` + ); } return await this[mobileCommandsMapping[mobileCommand]](opts); };