Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#228 fix: The extension of the Page Object design pattern #267

Merged
merged 16 commits into from
Nov 21, 2015
Merged

#228 fix: The extension of the Page Object design pattern #267

merged 16 commits into from
Nov 21, 2015

Conversation

TikhomirovSergey
Copy link
Contributor

Change list:

  • update to Selenium 2.48.2
  • The total refactoring of the io.appium.java_client.pagefactory package:
    • the interception of proxies was improved. Now invocation of Object methods (such as .getClass(), finalize()) which are not overriden by RemoteWebElement won't start the excessive searching for elements. The same is true for methods of WrapsDdriver/WrapsElement interfaces.
    • code fragments which were copied from Seleneium upstream (this things are very needfull and they were private) converted to overridden methods
    • some API which was private is public now and available for the re-using/extension
    • the extension of the Page Object design pattern was completed.
    • general improvemets of desktop browser compatibility tests
    • in order to cover the new feature with tests were added:

Use cases which are already provided

Information about Page Object design pattern.. Information about PageFactory.

WebElement/list of WebElement field can be populated by default:

@FindBy(someStrategy) //for browser or web view html UI 
//also for mobile native applications when other locator strategies are not defined
WebElement someElement;

@FindBy(someStrategy) //for browser or web view html UI 
//also for mobile native applications when other locator strategies are not defined
List<WebElement> someElements;

If there is need to use convinient locators for mobile native applications then the following is available:

@AndroidFindBy(someStrategy) //for Android UI when Android UI automator is used
AndroidElement someElement;

@AndroidFindBy(someStrategy) //for Android UI when Android UI automator is used
List<AndroidElement> someElements;

@SelendroidFindBy(someStrategy) //for Android UI when Selendroid automation is used
RemoteWebElement someElement;

@SelendroidFindBy(someStrategy) //for Android UI when Selendroid automation is used
List<RemoteWebElement> someElements;

@iOSFindBy(someStrategy) //for iOS native UI
IOSElement someElement;

@iOSFindBy(someStrategy) //for iOS native UI
List<IOSElement> someElements;

The example for the crossplatform mobile native testing

@AndroidFindBy(someStrategy) 
@iOSFindBy(someStrategy) 
MobileElement someElement;

@AndroidFindBy(someStrategy) //for the crossplatform mobile native
@iOSFindBy(someStrategy) //testing
List<MobileElement> someElements;

The fully cross platform examle

//the fully cross platform examle
@FindBy(someStrategy) //for browser or web view html UI
@AndroidFindBy(someStrategy) //for Android native UI 
@iOSFindBy(someStrategy)  //for iOS native UI 
RemoteWebElement someElement;

//the fully cross platform examle
@FindBy(someStrategy)
@AndroidFindBy(someStrategy) //for Android native UI 
@iOSFindBy(someStrategy)  //for iOS native UI 
List<RemoteWebElement> someElements;

Also it is possible to define chained or any possible locators.

  • Chained
@FindBys({@FindBy(someStrategy1), @FindBy(someStrategy2)}) 
@AndroidFindBys({@AndroidFindBy(someStrategy1), @AndroidFindBy(someStrategy2)}) 
@iOSFindBys({@iOSFindBy(someStrategy1), @iOSFindBy(someStrategy2)}) 
RemoteWebElement someElement;

@FindBys({@FindBy(someStrategy1), @FindBy(someStrategy2)}) 
@AndroidFindBys({@AndroidFindBy(someStrategy1), @AndroidFindBy(someStrategy2)}) 
@iOSFindBys({@iOSFindBy(someStrategy1), @iOSFindBy(someStrategy2)}) 
List<RemoteWebElement> someElements;
  • Any possible
@FindAll({@FindBy(someStrategy1), @FindBy(someStrategy2)}) 
@AndroidFindAll({@AndroidFindBy(someStrategy1), @AndroidFindBy(someStrategy2)}) 
@iOSFindAll({@iOSFindBy(someStrategy1), @iOSFindBy(someStrategy2)}) 
RemoteWebElement someElement;

@FindAll({@FindBy(someStrategy1), @FindBy(someStrategy2)}) 
@AndroidFindAll({@AndroidFindBy(someStrategy1), @AndroidFindBy(someStrategy2)}) 
@iOSFindAll({@iOSFindBy(someStrategy1), @iOSFindBy(someStrategy2)}) 
List<RemoteWebElement> someElements;

Appium Java client is integrated with Selenium PageFactory by AppiumFieldDecorator.

Examples of ways to populate object fields are below:

PageFactory.initElements(new AppiumFieldDecorator(searchContext /*WebDriver or WebElement
              instance */), 
              pageObject //an instance of PageObject.class
);

PageFactory.initElements(new AppiumFieldDecorator(searchContext, /*WebDriver or WebElement
              instance */
        15, //default implicit waiting timeout for all strategies
        TimeUnit.SECONDS), 
            pageObject //an instance of PageObject.class
);
PageFactory.initElements(new AppiumFieldDecorator(searchContext, /*WebDriver or WebElement
              instance */
        new TimeOutDuration(15, //default implicit waiting timeout for all strategies
        TimeUnit.SECONDS)), 
            pageObject //an instance of PageObject.class
);

If time of the waiting for elements differs from usual (longer, or shorter when element is needed only for quick checkings/assertions) then

@WithTimeout(timeOut = yourTime, timeUnit = yourTimeUnit)
RemoteWebElement someElement;

@WithTimeout(timeOut = yourTime, timeUnit = yourTimeUnit)
List<RemoteWebElement> someElements;

All mentioned use cases are provided and will be provided further. Below is the new additional option.

Details of the new feature

This feature was designed for the typifying of commonly used groups of elements which are nested in some root element. That should to allow end user the describe of pages/screens in terms of widgets instead of elements. This development was inspired by the Html Elements framework by Yandex. But there is no copy/paste and proposed code is completely original.

The simple example

Let's imagine that the task is to check an Android client of the http://www.rottentomatoes.com. Let it be like a picture below

Lets imagine that it is only a part of the screen.

A typical page object could look like:

public class RottenTomatoesScreen {
    //convinient locator  
    private List<AndroidElement> titles;

    //convinient locator  
    private List<AndroidElement> scores;

    //convinient locator  
    private List<AndroidElement> castings;
    //element declaration goes on 

    public String getMovieCount(){
       //.......
    }  

    public String getTitle(params){
       //.......
    }  

    public String getScore(params){
      //.......
    }  

    public String getCasting(params){
      //.......
    } 

    public void openMovieInfo(params){
      //.......
    } 

    //method declaration goes on 
}

The description above can be decomposed. Let's work it out!

Firstly a Movie-widget could be described this way:

import io.appium.java_client.pagefactory.Widget;
import org.openqa.selenium.WebElement;

public class Movie extends Widget{
   protected Movie(WebElement element) {
        super(element);
   }

   //convinient locator  
   private AndroidElement title;

   //convinient locator  
   private AndroidElement score;

   //convinient locator  
   private AndroidElement casting;

   public String getTitle(params){
       //.......
   }  

   public String getScore(params){
      //.......
   }  

   public String getCasting(params){
      //.......
   } 

   public void openMovieInfo(params){
       ((AndroidElement) getWrappedElement()).tap(1, 1500);
   } 

}

So, now page object looks

public class RottenTomatoesScreen {

     @AndroidFindBy(a locator which convinient to find a single movie-root - element)
     private List<Movie> movies;

      //element declaration goes on 

     public String getMovieCount(){
        return movies.size();
     }  

     public Movie getMovie(int index){
        //any interaction with sub-elements of a movie-element
        //will be performed outside of the page-object instance 
        return movie.get(index);
     }
     //method declaration goes on 
}

Ok. What if Movie-class is reused and a wrapped root element is usually found by the same locator?

Then

//the class is annotated !!!
@AndroidFindBy(a locator which convinient to find a single movie-root - element)
public class Movie extends Widget{
...
}

and

public class RottenTomatoesScreen {
     //!!! locator is not necessary at this case
     private List<Movie> movies;
...
}

Ok. What if movie list is not a whole screen? E.g. we want to descride it as a widget with nested movies.

Then:

//with the usual locator or without it
public class Movies extends Widget{

    //with a custom locator or without it
    private List<Movie> movies;
...
}

and

public class RottenTomatoesScreen {

    //with a custom locator or without it
    Movies movies;
...
}

Good! How to poputate all these fields?

As usual:

RottenTomatoesScreen screen = new RottenTomatoesScreen();
PageFactory.initElements(new AppiumFieldDecorator(searchContext /*WebDriver or WebElement
              instance */), screen);

Specification

A class which describes a widget or group of elements should extend

io.appium.java_client.pagefactory.Widget;

Any widget/group of elements can be described it terms of sub-elements or nested sub-widgets.
Appium-specific annotations are used for this purpose.

Any class which describes the real widget or group of elements can be annotated

That means that when the same "widget" is used frequently and any root element of this can be found by the same locator then user can

@FindBy(relevant locator) //how to find a root element
public class UsersWidget extends Widget{

  @FindBy(relevant locator) //this element will be found 
  //using the root element
  WebElement subElement1;

  @FindBy(relevant locator) //this element will be found 
  //using the root element
  WebElement subElement2;

  @FindBy(relevant locator) //a root element 
  //of this widget is the sub-element which 
  //will be found from top-element
  UsersWidget subWidget;

  //and so on..   
}

and then it is enough

  //above is the other field declaration

  UsersWidget widget;

  //below is the other field/method declaration

If the widget really should be found using an another locator then

  //above is the other field declaration
  @FindBy(another relevant locator) //this locator overrides 
  //the declared in the using class
  UsersWidget widget;

  //below is the other field/method declaration

Ok. What should users do if they want to implement a subclass which describes a similar group of elements for the same platform?

There is nothing special.

@FindBy(relevant locator) //how to find a root element
public class UsersWidget extends Widget{
... 
}
//at this case the root element will be found by the locator
//which is declared in superclass
public class UsersOverriddenWidget extends UsersWidget {
... 
}

and

@FindBy(relevant locator2) //this locator overrides 
//all locators declared in superclasses
public class UsersOverriddenWidget2 extends UsersWidget {
... 
}

Is it possible to reuse "widgets" in crossplatform testing?

If there is no special details of interaction with an application browser version and/or versions for different mobile OS's then

@FindBy(relevant locator for browser/webview html or by default) 
@AndroidFindBy(relevant locator for Android UI automator)
@iOSFindBy(relevant locator for Android UI automator)
public class UsersWidget extends Widget {

 @FindBy(relevant locator for browser/webview html or by default) 
 @AndroidFindBy(relevant locator for Android UI automator)
 @iOSFindBy(relevant locator for iOS UI automation)
  RemoteWebElement subElement1;

 @FindBy(relevant locator for browser/webview html or by default) 
 @AndroidFindBy(relevant locator for Android UI automator)
 @iOSFindBy(relevant locator for iOS UI automation)
  RemoteWebElement subElement2;

 @FindBy(relevant locator for browser/webview html or by default) //overrides a html/default
  //locator declared in the using class
 @AndroidFindBy(relevant locator for Android UI automator) //overrides an Android UI automator
  //locator declared in the using class
 @iOSFindBy(relevant locator for iOS UI automation) //overrides an iOS UI automation
  //locator declared in the using class
  UsersWidget subWidget;

  //and so on..   
}

What if interaction with a "widget" has special details for each used platform, but the same at high-level

Then it is possible

public /*abstract*/ class DefaultAbstractUsersWidget extends Widget{

}

and

@FindBy(locator)
public class UsersWidgetForHtml extends DefaultAbstractUsersWidget {

}

and

@AndroidFindBy(locator)
public class UsersWidgetForAndroid extends DefaultAbstractUsersWidget {

}

and even

@iOSFindBy(locator)
public class UsersWidgetForIOS extends DefaultAbstractUsersWidget {

}

and then

  import io.appium.java_client.pagefactory.OverrideWidget;
  ...

  //above is the other field declaration
  @OverrideWidget(html = UsersWidgetForHtml.class, 
  androidUIAutomator = UsersWidgetForAndroid.class, iOSUIAutomation = UsersWidgetForIOS .class)
  DefaultAbstractUsersWidget widget;

  //below is the other field/method declaration

This use case has some restrictions;

  • All classes which are declared by the OverrideWidget annotation should be subclasses of the class declared by field
  • All classes which are declared by the OverrideWidget should not be abstract. If a declared class is overriden partially like
  //above is the other field declaration

  @OverrideWidget(iOSUIAutomation = UsersWidgetForIOS .class)
  DefaultUsersWidget widget; //lets assume that there differences in interaction with iOS 
  //and by default we use DefaultUsersWidget. Then DefaultUsersWidget should not be abstract 
  //too.
  //

  //below is the other field/method declaration
  • for now it is not possible to
  import io.appium.java_client.pagefactory.OverrideWidget;
  ...

  //above is the other field declaration
  @OverrideWidget(html = UsersWidgetForHtml.class, 
  androidUIAutomator = UsersWidgetForAndroid.class, iOSUIAutomation = UsersWidgetForIOS .class)
  DefaultAbstractUsersWidget widget;

  //below is the other field/method declaration

  //user's code
  ((UsersWidgetForAndroid) widget).doSpecialWorkForAndroing()

I think that this problem will be resolved next time.

Good! What about widget lists?

All that has been mentioned above is true for "widget" lists.

One more restriction

It is strongly recommended to implement each subclass of io.appium.java_client.pagefactory.Widget with this constructor

   public /*or any other available modifier*/ WidgetSubclass(WebElement element) {
       super(element);
   }

@TikhomirovSergey
Copy link
Contributor Author

#228

@TikhomirovSergey
Copy link
Contributor Author

@Jonahss @bootstraponline please look at this. Do you like this PR? :)

@Simon-Kaz, @saikrishna321 you are invited too :) Please look at this before it will have been merged

@@ -87,7 +87,6 @@ More can be found in the docs, but here's a quick list of features which this pr
- lockScreen()
- isLocked()
- shake()
- complexFind()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think COMPLEX_FIND is still supported?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was removed here
329e84e

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

complexfind is still referenced in MobileCommand.java . is this done on purpose?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rompic
It is need to think about. Anyway it is not in scope of this PR.
And anyway it should be removed if it useless.

@bootstraponline
Copy link
Member

I'm really impressed with this pull request. I think end users will benefit from a robust page object implementation. It's also fantastic that the PR includes tests so people know how to use the feature.

I'm worried that some of the coding styles adopted in java-client easily lend themselves to bugs. For example, I think we're better off always using braces for if and if/else, even if they're only single lines.

In general I'm a fan of the effective Java approach and Google's style guide. I know it's outside the scope of this PR, however it'd be nice to integrate static analysis tools into the java-client pull request workflow similar to how appium and appium's ruby bindings run lint tools automatically. For Java, I'd recommend checking out google-java-format

I think as the code base continues to grow and people contribute, there's an increasing risk of diminishing quality without adopting automatic style formatting.

@TikhomirovSergey
Copy link
Contributor Author

@bootstraponline
I fixed code style & format issues.
Yep. I'm sharing your point of view. In order to not lose it I've created this ticket. #269

@rompic
Copy link
Contributor

rompic commented Nov 13, 2015

Nice. I don't realen understand the chained example. Can you provide an example?

@rompic
Copy link
Contributor

rompic commented Nov 14, 2015

I meant the expected result is not really clear to me. Does it look for all elements using the defined strategies?

Btw. I noticed a typo in the first example where some of the methods are called reviev instead of review.

great work!

@TikhomirovSergey
Copy link
Contributor Author

@rompic

It means that all locators are used by a chain of searchings. E.g. some element is expected to be found by the first defined by-strategy, a next element should be found from this one and by the second defined by-strategy and so on. The expected result is an element or list of elements found by the last defined by-strategy from a previously found elemen in this chain.

FindByAll means that an element or list of elements could be found by any (!!!) of defined locators. There is no chain like above. If the using of some locator has a positive result at this moment (an element is found/size of a list is greater than zero) the the searching is being stopped and the result is being returned.

I'm suspecting that there is the naming issue. But this naming was borrowed and it is convinient to Selenium project.

Thank you for the remark. This typo will be improved.

@SButterfly
Copy link

Hello, I'm new in Appium.
And I'd like to know, is it possible to get different elements but with the same class name? I mean that there are several elements in the layer with the same class name, and I want to get all of them.

Ex. Let the structure of my classes be like:

public class ChatMessages {

    @iOSFindBy(className = "UIATableCell")
    private List<Message> messageList;
}

public class Message extends Widget {

    public Message(WebElement element) {
        super(element);
    }

    @iOSFindBy(className = "UIAStaticText")
    public IOSElement userNameElement;

    @iOSFindBy(className = "UIAStaticText")
    public IOSElement messageElement;
}

And all I know that messageElement is the second in cell. How can I get valid element?

@SButterfly
Copy link

Finally I did it.

public class Message extends Widget {

    public Message(WebElement element) {
        super(element);
    }

    @iOSFindBy(uiAutomator = ".staticTexts()[0]")
    private IOSElement userNameElement;

    @iOSFindBy(uiAutomator = ".staticTexts()[1]")
    private IOSElement messageElement;
}

But I'm still looking other ways to do this.

@TikhomirovSergey
Copy link
Contributor Author

Hi @SButterfly
It is difficult to react to replies on closed PR's.
If there are some problems please open an issue here https://github.com/appium/java-client/issues

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants