Skip to content

Commit

Permalink
Added Appium PageFactory
Browse files Browse the repository at this point in the history
  • Loading branch information
martingrossmann committed Mar 24, 2023
1 parent 989c827 commit 4291562
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 2 deletions.
4 changes: 4 additions & 0 deletions appium/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ dependencies {
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'org.apache.commons:commons-lang3:3.12.0'

// For AppiumClassFinder
implementation 'org.reflections:reflections:0.9.12'

testImplementation 'io.testerra:driver-ui:' + testerraTestVersion
// testImplementation 'io.testerra:driver-ui'
testImplementation 'io.testerra:report-ng:' + testerraTestVersion
testImplementation 'io.appium:java-client:7.3.0'
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package eu.tsystems.mms.tic.testframework.mobile.pageobject;

import eu.tsystems.mms.tic.testframework.logging.Loggable;
import eu.tsystems.mms.tic.testframework.mobile.driver.MobileOsChecker;
import eu.tsystems.mms.tic.testframework.pageobjects.Page;
import eu.tsystems.mms.tic.testframework.pageobjects.PageObject;
import org.openqa.selenium.Platform;
import org.openqa.selenium.WebDriver;
import org.reflections.Reflections;
import org.reflections.scanners.SubTypesScanner;
import org.reflections.util.ClasspathHelper;
import org.reflections.util.ConfigurationBuilder;

import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Created on 2023-03-13
*
* @author mgn
*/
public class AppiumClassFinder implements Loggable {

private static AppiumClassFinder INSTANCE;

/**
* This call takes some time. It has an impact to the duration of the first page check (takes ca 2-3 seconds longer).
*/
private final Reflections reflections = new Reflections(filter(configure()));

private AppiumClassFinder() {
}

public static AppiumClassFinder getInstance() {
if (INSTANCE == null) {
INSTANCE = new AppiumClassFinder();
}
return INSTANCE;
}

public <T extends Page> Class<T> getBestMatchingClass(Class<T> baseClass, WebDriver webDriver) {
MobileOsChecker osChecker = new MobileOsChecker();
Platform platform = osChecker.getPlatform(webDriver);

Class<? extends PageObject> matchedClass = AppiumClassFinder.Caches.getCache(baseClass, platform);

if (matchedClass == null) {
// scan and fill cache
this.findSubPagesOf(baseClass);
matchedClass = AppiumClassFinder.Caches.getCache(baseClass, platform);

if (matchedClass == null) {
throw new RuntimeException("Something went wrong scanning this class for sub types: " + baseClass.getName());
}
}

return (Class<T>) matchedClass;
}

private <T extends PageObject> void findSubPagesOf(final Class<T> pageClass) {
log().debug("Searching for subtypes of class {}", pageClass);
Class<T> baseClass = null;
if (!Modifier.isAbstract(pageClass.getModifiers())) {
baseClass = pageClass;
AppiumClassFinder.Caches.setCache(baseClass, this.getMatchingPlatform(baseClass.getSimpleName()), baseClass);
}

Set<? extends Class<T>> subClasses = reflections.getSubTypesOf((Class) baseClass);
for (Class<T> subClass : subClasses) {
String subClassName = subClass.getSimpleName();

if (Modifier.isAbstract(subClass.getModifiers())) {
log().debug("Not taking {} into consideration, because it is abstract", subClassName);
} else {
if (this.matchingPattern(subClassName, baseClass.getSimpleName())) {
AppiumClassFinder.Caches.setCache(baseClass, this.getMatchingPlatform(subClassName), subClass);
}
}
}
}

public void clearCache() {
AppiumClassFinder.Caches.IMPLEMENTATIONS_CACHE.clear();
}

private boolean matchingPattern(String className, String baseClassName) {
Pattern pattern = Pattern.compile("(?i)^(ios|android)" + baseClassName);
Matcher matcher = pattern.matcher(className);
return matcher.find();
}

private Platform getMatchingPlatform(String className) {
if (className.toLowerCase().contains(Platform.IOS.toString().toLowerCase())) {
return Platform.IOS;
}
if (className.toLowerCase().contains(Platform.ANDROID.toString().toLowerCase())) {
return Platform.ANDROID;
}
return Platform.ANY;
}

private ConfigurationBuilder configure() {
return new ConfigurationBuilder().setUrls(ClasspathHelper.forJavaClassPath());
}

/**
* This method should prune resources we are not interested in, but not change the interesting results.
*/
private ConfigurationBuilder filter(final ConfigurationBuilder configuration) {
configuration.setScanners(new SubTypesScanner()); // drops TypeAnnotationScanner
configuration.useParallelExecutor();
return configuration;
}

/**
* Store already found classes in a map like:
* MainClass.class: [
* Platform.ANY: MainClass.class
* Platform.IOS: IOSMainClass.class
* Platform.ANDROID: AndroidMainClass.class
* ]
*/
private static class Caches {

private static final Platform DEFAULT_PLATFORM = Platform.ANY;

private static final Map<Class<? extends PageObject>, Map<Platform, Class<? extends PageObject>>> IMPLEMENTATIONS_CACHE = new ConcurrentHashMap<>();

private static Class<? extends PageObject> getCache(Class<? extends PageObject> pageClass, Platform platform) {
if (platform != Platform.ANDROID && platform != Platform.IOS) {
platform = DEFAULT_PLATFORM;
}

synchronized (IMPLEMENTATIONS_CACHE) {
if (!IMPLEMENTATIONS_CACHE.containsKey(pageClass)) {
return null;
}

Map<Platform, Class<? extends PageObject>> map = IMPLEMENTATIONS_CACHE.get(pageClass);
Class<? extends PageObject> matchedClass = map.get(platform);
if (matchedClass == null) {
matchedClass = map.get(DEFAULT_PLATFORM);
}
return matchedClass;
}
}

/**
* Returns the best matching class from map. If no platform specific map exists, the baseclass will return.
*/
private static void setCache(Class<? extends PageObject> pageClass, Platform platform, Class<? extends PageObject> prioritizedClassInfos) {
if (platform != Platform.ANDROID && platform != Platform.IOS) {
platform = DEFAULT_PLATFORM;
}

synchronized (IMPLEMENTATIONS_CACHE) {
if (!IMPLEMENTATIONS_CACHE.containsKey(pageClass)) {
IMPLEMENTATIONS_CACHE.put(pageClass, new HashMap<>());
}
Map<Platform, Class<? extends PageObject>> map = IMPLEMENTATIONS_CACHE.get(pageClass);

map.put(platform, prioritizedClassInfos);
}
}
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package eu.tsystems.mms.tic.testframework.mobile.pageobject;

import eu.tsystems.mms.tic.testframework.enums.CheckRule;
import eu.tsystems.mms.tic.testframework.pageobjects.Page;
import eu.tsystems.mms.tic.testframework.pageobjects.internal.DefaultPageFactory;
import eu.tsystems.mms.tic.testframework.pageobjects.internal.PageFactory;
import org.openqa.selenium.WebDriver;

/**
* Created on 2023-03-13
*
* @author mgn
*/
public class AppiumPageFactory extends DefaultPageFactory {

@Override
public <T extends Page> T createPageWithCheckRule(Class<T> pageClass, WebDriver webDriver, CheckRule checkRule) {
return super.createPageWithCheckRule(AppiumClassFinder.getInstance().getBestMatchingClass(pageClass, webDriver), webDriver, checkRule);
}

@Override
public PageFactory clearThreadLocalPagesPrefix() {
AppiumClassFinder.getInstance().clearCache();
return super.clearThreadLocalPagesPrefix();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package eu.tsystems.mms.tic.testframework.mobile.systemundertest.ioc;

import com.google.inject.AbstractModule;
import com.google.inject.Scopes;
import eu.tsystems.mms.tic.testframework.mobile.pageobject.AppiumPageFactory;
import eu.tsystems.mms.tic.testframework.pageobjects.internal.PageFactory;

/**
* Created on 2023-03-13
*
* @author mgn
*/
public class AppiumConnectorTestModule extends AbstractModule {

protected void configure() {
bind(PageFactory.class).to(AppiumPageFactory.class).in(Scopes.SINGLETON);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package eu.tsystems.mms.tic.testframework.mobile.systemundertest.page.nativeos;

import org.openqa.selenium.WebDriver;

/**
* Created on 2023-03-13
*
* @author mgn
*/
public class IosWifiSettingsPage extends WifiSettingsPage{
public IosWifiSettingsPage(WebDriver webDriver) {
super(webDriver);
log().info("You are using the specific IOS WifiSettings page.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
public class TesterraMobileAppTest extends AbstractAppiumTest {

@Test
public void testT01AndroidApp() throws MalformedURLException {
public void testT01AndroidApp() {
AppiumDriverRequest request = new AppiumDriverRequest();
request.setDeviceQuery("contains(@name, 'Galaxy S20')");
request.getDesiredCapabilities().setCapability("appiumVersion", "1.22.3");
Expand Down Expand Up @@ -74,6 +74,23 @@ public void testT10NativeIOSSettings() {
UITestUtils.takeScreenshots();
}

@Test
public void testT12NativeAndWebAccessIOS() {
AppiumDriverRequest request = new AppiumDriverRequest();
request.setDeviceQuery("contains(@name, 'iPhone X')");
request.getDesiredCapabilities().setCapability("appiumVersion", "1.22.3");
request.setAppiumEngine("XCUITest");
WebDriver webDriver = WEB_DRIVER_MANAGER.getWebDriver(request);



WifiSettingsPage wifiSettingsPage = openWifiSettings(webDriver);
switchWiFi(wifiSettingsPage);


UITestUtils.takeScreenshots();
}

@Test
public void testT11NativeAndroidSettings() {
AppiumDriverRequest request = new AppiumDriverRequest();
Expand Down
2 changes: 1 addition & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ rootProject.name = 'testerra-appium-connector'
include 'appium'
include 'appium-seetest'

//includeBuild("../testerra")
//includeBuild("../github_testerra")

0 comments on commit 4291562

Please sign in to comment.