All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.axway.ats.uiengine.MobileDriver Maven / Gradle / Ivy

/*
 * Copyright 2017 Axway Software
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.axway.ats.uiengine;

import java.io.File;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;

import org.apache.log4j.Logger;
import org.openqa.selenium.Dimension;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.remote.DesiredCapabilities;

import com.axway.ats.common.PublicAtsApi;
import com.axway.ats.common.system.OperatingSystemType;
import com.axway.ats.core.process.LocalProcessExecutor;
import com.axway.ats.core.process.model.IProcessExecutor;
import com.axway.ats.core.utils.IoUtils;
import com.axway.ats.core.utils.HostUtils;
import com.axway.ats.core.utils.StringUtils;
import com.axway.ats.uiengine.engine.MobileEngine;
import com.axway.ats.uiengine.exceptions.MobileOperationException;
import com.axway.ats.uiengine.exceptions.NotSupportedOperationException;
import com.axway.ats.uiengine.internal.driver.InternalObjectsEnum;
import com.axway.ats.uiengine.utilities.mobile.MobileDeviceUtils;

import io.appium.java_client.AppiumDriver;
import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.ios.IOSDriver;

/**
 * A driver used for working with a mobile applications(Android or iOS)
 *
 * 

* User guide pages related to this class:
* UI Engine basics * and * testing mobile applications */ @PublicAtsApi public class MobileDriver extends UiDriver { private static final Logger log = Logger.getLogger( MobileDriver.class ); @PublicAtsApi public static final String DEVICE_ANDROID_EMULATOR = "Android Emulator"; @PublicAtsApi public static final String DEVICE_ANDROID = "Android"; @PublicAtsApi public static final String DEVICE_IPHONE_SIMULATOR = "iPhone Simulator"; @PublicAtsApi public static final String DEVICE_IPAD_SIMULATOR = "iPad Simulator"; @PublicAtsApi public static final String NATIVE_CONTEXT = "NATIVE_APP"; private static final String ANDROID_HOME_ENV_VAR = "ANDROID_HOME"; private static final int MAX_ADB_RELATED_RETRIES = 2; private static final int DEFAULT_APPIUM_PORT = 4723; private AppiumDriver driver; private String deviceName = null; private String platformVersion = null; private String udid = null; private String host = null; private int port = -1; private MobileEngine mobileEngine; private boolean isWorkingRemotely = false; private boolean isAndroidAgent = false; private String androidHome = null; private String adbLocation = null; private Dimension screenDimensions; private MobileDeviceUtils mobileDeviceUtils; public MobileDriver( String deviceName, String platformVersion, String udid ) { this( deviceName, platformVersion, udid, null, DEFAULT_APPIUM_PORT ); } public MobileDriver( String deviceName, String platformVersion, String udid, String host ) { this( deviceName, platformVersion, udid, host, DEFAULT_APPIUM_PORT ); } public MobileDriver( String deviceName, String platformVersion, String udid, String host, int port ) { this.deviceName = deviceName; this.platformVersion = platformVersion; this.port = port; this.udid = udid; if( host == null ) { this.host = HostUtils.LOCAL_HOST_IPv4; } else { this.host = host; this.isWorkingRemotely = !HostUtils.isLocalHost( host ); } this.isAndroidAgent = deviceName.toLowerCase().contains( "android" ); this.mobileDeviceUtils = new MobileDeviceUtils( this ); } /** * * @return whether the target agent is Android, otherwise it is iOS */ public boolean isAndroidAgent() { return isAndroidAgent; } public String getHost() { return host; } public boolean isWorkingRemotely() { return isWorkingRemotely; } /** * * @param androidHome example: "d:\\android\\android-sdk\\" */ @PublicAtsApi public void setAndroidHome( String androidHome ) { if( androidHome != null ) { this.androidHome = androidHome; this.adbLocation = IoUtils.normalizeDirPath( this.androidHome ) + "platform-tools/"; String pathToADBExecutable = null; if( this.mobileDeviceUtils.isAdbOnWindows() ) { pathToADBExecutable = this.adbLocation + "adb.exe"; } else { pathToADBExecutable = this.adbLocation + "adb"; } if( !isWorkingRemotely && !new File( pathToADBExecutable ).exists() ) { this.adbLocation = null; } } } /** * Start session to device * @deprecated Use {@link #start(String)} method instead. */ @Override @Deprecated public void start() { } /** * Start session to device and load the application
* @param appPath the absolute path to the application: *
     *       iOS: absolute path to simulator-compiled .app file or the bundle_id of the desired target on device
     *       Android: absolute path to .apk file
     * 
*/ @PublicAtsApi public void start( String appPath ) { log.info( "Starting mobile testing session to device: " + getDeviceDescription() ); try { // http://appium.io/slate/en/master/?java#appium-server-capabilities DesiredCapabilities desiredCapabilities = new DesiredCapabilities(); desiredCapabilities.setCapability( "automationName", "Appium" ); if( isAndroidAgent ) { // start emulator: .../sdk/tools/emulator -avd vmname // install application: .../sdk/platform-tools/adb install /usr/local/apps/SecureTransportMobile.apk if( this.adbLocation == null ) { // try to set Android home and adb location from the ANDROID_HOME environment variable readAndroidHomeFromEnvironment(); if( this.adbLocation == null ) { throw new MobileOperationException( "You must specify a valid Android home location or define " + ANDROID_HOME_ENV_VAR + " environment variable. The ADB executable must be located in a 'platform-tools/' subfolder" ); } } desiredCapabilities.setCapability( "platformName", "Android" ); } else { desiredCapabilities.setCapability( "platformName", "iOS" ); } desiredCapabilities.setCapability( "deviceName", deviceName ); desiredCapabilities.setCapability( "platformVersion", this.platformVersion ); if( !StringUtils.isNullOrEmpty( this.udid ) ) { desiredCapabilities.setCapability( "udid", this.udid ); } desiredCapabilities.setCapability( "app", appPath ); desiredCapabilities.setCapability( "autoLaunch", true ); desiredCapabilities.setCapability( "newCommandTimeout", 30 * 60 ); desiredCapabilities.setCapability( "noReset", true ); // don’t reset settings and app state before this session // desiredCapabilities.setCapability( "fullReset", true ); // clean all Android/iOS settings (iCloud settings), close and uninstall the app URL url = new URL( "http://" + this.host + ":" + this.port + "/wd/hub" ); if( isAndroidAgent ) { driver = new AndroidDriver( url, desiredCapabilities ); } else { driver = new IOSDriver( url, desiredCapabilities ); } driver.setLogLevel( Level.ALL ); // the following timeout only works for NATIVE context, but we will handle it in MobileElementState. // Also there is a problem when != 0. In some reason, for iOS only(maybe), this timeout acts as session timeout ?!? driver.manage().timeouts().implicitlyWait( 0, TimeUnit.MILLISECONDS ); // driver.manage().timeouts().pageLoadTimeout( 30000, TimeUnit.MILLISECONDS ); // UnsupportedCommandException // driver.manage().timeouts().setScriptTimeout( 10000, TimeUnit.MILLISECONDS ); // WebDriverException: Not yet implemented driver.context( NATIVE_CONTEXT ); this.screenDimensions = driver.manage().window().getSize(); // must be called in NATIVE context mobileEngine = new MobileEngine( this, this.mobileDeviceUtils ); } catch( Exception e ) { throw new MobileOperationException( "Error starting connection to device and application under test." + " Check if there is connection to device and the Appium server is running.", e ); } } @Override public void stop() { log.info( "Stopping mobile testing session to device: " + getDeviceDescription() ); driver.quit(); } /** * Stop application by package name and the session to device * @deprecated Use {@link #stop()} method instead * * @param applicationPackage application package name */ @PublicAtsApi @Deprecated public void stop( String applicationPackage ) { stop(); } /** * Clear application cache * * @param applicationPackage application package name */ @PublicAtsApi public void clearAppCache( String applicationPackage ) { if( isAndroidAgent ) { // Clear application cache using ADB command: ./adb shell pm clear <PACKAGE>
// for example: ./adb shell pm clear com.axway.st.mobile if( this.adbLocation == null ) { throw new MobileOperationException( "You must specify a valid Android home location or define " + ANDROID_HOME_ENV_VAR + " environment variable. The ADB executable must be located in a 'platform-tools/' subfolder" ); } String[] commandArguments = new String[]{ "shell", "pm", "clear", applicationPackage }; IProcessExecutor pe = null; int numRetries = 0; while( numRetries <= MAX_ADB_RELATED_RETRIES ) { if( numRetries > 0 ) { log.warn( "Retrying to start application action as previous try failed" ); } try { pe = this.mobileDeviceUtils.executeAdbCommand( commandArguments, false ); } catch( Exception e ) { throw new MobileOperationException( "Unable to clear application cache of '" + applicationPackage + "'", e ); } numRetries++; if( pe.getExitCode() == 0 ) { break; } else { if( numRetries <= MAX_ADB_RELATED_RETRIES ) { log.error( "Unable to clear application cache of '" + applicationPackage + "'. Start command failed (Exit code: " + pe.getExitCode() + ", STDOUT: '" + pe.getStandardOutput() + "', STDERR: '" + pe.getErrorOutput() + "')" ); //try to kill ADB and issue start again killAdbServer(); } else { throw new MobileOperationException( "Unable to clear application cache of '" + applicationPackage + "'. Clear cache command failed (Exit code: " + pe.getExitCode() + ", STDOUT: '" + pe.getStandardOutput() + "', STDERR: '" + pe.getErrorOutput() + "')" ); } } } } else { //TODO: Find solution. Note that "this.driver.resetApp();" doesn't reset app cache. throw new NotSupportedOperationException( "Currently clear application cache operation for iOS is not implemented" ); } } @PublicAtsApi public MobileEngine getMobileEngine() { return mobileEngine; } /** * NOTE: This method should not be used directly into the test scripts. * The implementation may be changed by the Automation Framework Team without notice. * @return Internal Object */ public Object getInternalObject( String objectName ) { //NOTE: we use a String argument 'objectName' not directly an InternalObjectsEnum object, because we want to // hide from the end users this method and his usage switch( InternalObjectsEnum.getEnum( objectName ) ){ case WebDriver: //returns instance of Appium driver operating over Native/HTML elements return this.driver; default: break; } return null; } /** * * @return android home location */ public String getAndroidHome() { return androidHome; } /** * * @return adb location */ public String getAdbLocation() { return adbLocation; } /** * * @param screenshotOnError whether to create screenshot on error */ public void setScreenshotOnError( boolean screenshotOnError ) { // driver.setScreenshotOnError( screenshotOnError ); // TODO: implement it throw new NotSupportedOperationException( "Not implemented" ); } public Dimension getScreenDimensions() { return screenDimensions; } /** * Read ANDROID_HOME environment variable and set ADB location */ private void readAndroidHomeFromEnvironment() { try { setAndroidHome( System.getenv( ANDROID_HOME_ENV_VAR ) ); } catch( SecurityException se ) { log.warn( "No access to the process environment. Unable to read environment variable '" + ANDROID_HOME_ENV_VAR + "'" ); } } /** * Start application using ADB command: ./adb shell am start -W -S -n <ACTIVITY>
* for example: ./adb shell am start -W -S -n com.axway.st.mobile/.MobileAccessPlus * * @param activity application activity name */ private void startAndroidApplication( String activity ) { log.info( "Starting application with activity '" + activity + "' on device: " + getDeviceDescription() ); String[] commandArguments = new String[]{ "shell", "am", "start", "-W", "-S", "-n", activity }; int numRetries = 0; IProcessExecutor pe = null; while( numRetries <= MAX_ADB_RELATED_RETRIES ) { if( numRetries > 0 ) { log.info( "Retrying to start application action as previous try failed" ); } try { pe = this.mobileDeviceUtils.executeAdbCommand( commandArguments, false ); } catch( Exception e ) { throw new MobileOperationException( "Unable to start Android application with activity '" + activity + "'", e ); } numRetries++; if( pe.getExitCode() == 0 ) { break; } else { if( numRetries <= MAX_ADB_RELATED_RETRIES ) { log.error( "Unable to start Android application with activity '" + activity + "'. Start command failed (Exit code: " + pe.getExitCode() + ", STDOUT: '" + pe.getStandardOutput() + "', STDERR: '" + pe.getErrorOutput() + "')" ); //try to kill ADB and issue start again killAdbServer(); } else { throw new MobileOperationException( "Unable to start Android application with activity '" + activity + "'. Start command failed (STDOUT: '" + pe.getStandardOutput() + "', STDERR: '" + pe.getErrorOutput() + "')" ); } } } } /** * Stop application using ADB command: ./adb shell am force-stop <PACKAGE>
* for example: ./adb shell am force-stop com.axway.st.mobile * * @param applicationPackage application package */ private void stopAndroidApplication( String applicationPackage ) { log.info( "Stopping application '" + applicationPackage + "' on device: " + getDeviceDescription() ); String[] commandArguments = new String[]{ "shell", "am", "force-stop", applicationPackage }; IProcessExecutor pe = null; int numRetries = 0; while( numRetries <= MAX_ADB_RELATED_RETRIES ) { if( numRetries > 0 ) { log.warn( "Retrying to start application action as previous try failed" ); } try { pe = this.mobileDeviceUtils.executeAdbCommand( commandArguments, false ); } catch( Exception e ) { throw new MobileOperationException( "Unable to stop Android application with package '" + applicationPackage + "'", e ); } numRetries++; if( pe.getExitCode() == 0 ) { break; } else { if( numRetries <= MAX_ADB_RELATED_RETRIES ) { log.error( "Unable to stop Android application with package '" + applicationPackage + "'. Stop command failed (Exit code: " + pe.getExitCode() + ", STDOUT: '" + pe.getStandardOutput() + "', STDERR: '" + pe.getErrorOutput() + "')" ); // try to kill ADB and issue stop again killAdbServer(); } else { throw new MobileOperationException( "Unable to stop Android application with package '" + applicationPackage + "'. Stop command failed (Exit code: " + pe.getExitCode() + ", STDOUT: '" + pe.getStandardOutput() + "', STDERR: '" + pe.getErrorOutput() + "')" ); } } } // while } /** *
     * Start iOS application using ios-sim command: ios-sim launch <PATH TO APPLICATION.APP> --timeout 60 --exit
* for example: ios-sim launch /tmp/test/MobileAccessPlus.app --timeout 60 --exit *
* This command also starts the iOS Simulator if it's not already started. *
* Check here how to install ios-sim: https://github.com/phonegap/ios-sim#installation *
* @param appPath path to the application .app file */ private void startIOSApplication( String appPath ) { log.info( "Starting application '" + appPath + "' on device: " + getDeviceDescription() ); String[] commandArguments = new String[]{ "launch", appPath, "--timeout", "60", "--exit" }; IProcessExecutor pe = null; try { pe = getProcessExecutorImpl( "ios-sim", commandArguments ); pe.execute(); } catch( Exception e ) { throw new MobileOperationException( "Unable to start iOS application '" + appPath + "'", e ); } if( pe.getExitCode() != 0 ) { throw new MobileOperationException( "Unable to start iOS application '" + appPath + "'. Start command failed (STDOUT: '" + pe.getStandardOutput() + "', STDERR: '" + pe.getErrorOutput() + "')" ); } } /** * Stopping iOS Simulator */ @PublicAtsApi public void stopIOSSimulator() { log.info( "Stopping simulator on: " + getDeviceDescription() ); IProcessExecutor pe = null; try { // the simulator window was named "iPhone Simulator" till Xcode 6, now it is "iOS Simulator" pe = getProcessExecutorImpl( "killall", new String[]{ "iPhone Simulator", "iOS Simulator" } ); pe.execute(); } catch( Exception e ) { throw new MobileOperationException( "Unable to stop iOS Simulator", e ); } if( pe.getExitCode() != 0 ) { throw new MobileOperationException( "Unable to stop iOS Simulator. Stop command failed (STDOUT: '" + pe.getStandardOutput() + "', STDERR: '" + pe.getErrorOutput() + "')" ); } } /** * * @param command the command to run * @param commandArguments command arguments * @return {@link IProcessExecutor} implementation instance */ private IProcessExecutor getProcessExecutorImpl( String command, String[] commandArguments ) { if( isWorkingRemotely ) { String remoteProcessExecutorClassName = "com.axway.ats.action.processes.RemoteProcessExecutor"; try { Class remotePEClass = Class.forName( remoteProcessExecutorClassName ); Constructor constructor = remotePEClass.getDeclaredConstructors()[0]; return ( IProcessExecutor ) constructor.newInstance( host, command, commandArguments ); } catch( Exception e ) { throw new RuntimeException( "Unable to instantiate RemoteProcessExecutor. Check whether ATS Action " + "library and ATS Agent client are added as dependencies in classpath. " + "They are needed in order to invoke code on remote machine.", e ); } } return new LocalProcessExecutor( HostUtils.LOCAL_HOST_NAME, command, commandArguments ); } /** * * @return device details as {@link String} */ private String getDeviceDescription() { return deviceName + ( host != null ? ", host: " + host : "" ) + ( port > 0 ? ", port: " + port : "" ); } /** * Kill misbehaving ADB server in order to bring it back later to normal state. The server is automatically started on next ADB command. * Then failed ADB operation (start/stop/clear cache, etc. should be retried.) */ private void killAdbServer() { log.info( "Trying to restart ADB server on device: " + getDeviceDescription() ); String[] commandArguments = new String[]{ "kill-server" }; IProcessExecutor pe = null; try { pe = this.mobileDeviceUtils.executeAdbCommand( commandArguments, false ); } catch( Exception e ) { throw new MobileOperationException( "Unable to stop ADB server. 'adb kill-server' failed", e ); } if( pe.getExitCode() != 0 ) { log.warn( "Unable to stop gracefully the ADB server. Command failed (Exit code: " + pe.getExitCode() + ", STDOUT: '" + pe.getStandardOutput() + "', STDERR: '" + pe.getErrorOutput() + "')" ); log.info( "Trying to forcefully terminate ADB process" ); // fallback to taskkill try { if( OperatingSystemType.getCurrentOsType().isWindows() ) { pe = new LocalProcessExecutor( HostUtils.LOCAL_HOST_NAME, "taskkill.exe", new String[]{ "/IM", "adb.exe", "/F", "/T" } ); } else { pe = new LocalProcessExecutor( HostUtils.LOCAL_HOST_NAME, "killall", new String[]{ "adb" } ); } pe.execute(); } catch( Exception e ) { log.info( "Unable to kill ADB server. Command failed (Exit code: " + pe.getExitCode() + ", STDOUT: '" + pe.getStandardOutput() + "', STDERR: '" + pe.getErrorOutput() + "')" ); throw new MobileOperationException( "Unable to stop ADB server with taskkill/killall", e ); } if( pe.getExitCode() != 0 ) { // TODO - research possible error codes for non-existing process to kill log.error( "Unable to kill ADB server. Command failed (Exit code: " + pe.getExitCode() + ", STDOUT: '" + pe.getStandardOutput() + "', STDERR: '" + pe.getErrorOutput() + "')" ); } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy