Skip to content

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

Merged
TikhomirovSergey merged 16 commits into
appium:masterfrom
TikhomirovSergey:#228-fix
Nov 21, 2015
Merged

#228 fix: The extension of the Page Object design pattern#267
TikhomirovSergey merged 16 commits into
appium:masterfrom
TikhomirovSergey:#228-fix

Conversation

@TikhomirovSergey

Copy link
Copy Markdown
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 definedWebElementsomeElement;
@FindBy(someStrategy) //for browser or web view html UI //also for mobile native applications when other locator strategies are not definedList<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 usedAndroidElementsomeElement;
@AndroidFindBy(someStrategy) //for Android UI when Android UI automator is usedList<AndroidElement> someElements;
@SelendroidFindBy(someStrategy) //for Android UI when Selendroid automation is usedRemoteWebElementsomeElement;
@SelendroidFindBy(someStrategy) //for Android UI when Selendroid automation is usedList<RemoteWebElement> someElements;
@iOSFindBy(someStrategy) //for iOS native UIIOSElementsomeElement;
@iOSFindBy(someStrategy) //for iOS native UIList<IOSElement> someElements;

The example for the crossplatform mobile native testing

@AndroidFindBy(someStrategy) @iOSFindBy(someStrategy) MobileElementsomeElement;
@AndroidFindBy(someStrategy) //for the crossplatform mobile native@iOSFindBy(someStrategy) //testingList<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 RemoteWebElementsomeElement;
//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)}) RemoteWebElementsomeElement;
@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)}) RemoteWebElementsomeElement;
@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(newAppiumFieldDecorator(searchContext/*WebDriver or WebElement instance */), pageObject//an instance of PageObject.class
);

PageFactory.initElements(newAppiumFieldDecorator(searchContext, /*WebDriver or WebElement instance */15, //default implicit waiting timeout for all strategiesTimeUnit.SECONDS), pageObject//an instance of PageObject.class
);
PageFactory.initElements(newAppiumFieldDecorator(searchContext, /*WebDriver or WebElement instance */newTimeOutDuration(15, //default implicit waiting timeout for all strategiesTimeUnit.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)
RemoteWebElementsomeElement;
@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:

publicclassRottenTomatoesScreen {
//convinient locator privateList<AndroidElement> titles;
//convinient locator privateList<AndroidElement> scores;
//convinient locator privateList<AndroidElement> castings;
//element declaration goes on publicStringgetMovieCount(){
//.......
} publicStringgetTitle(params){
//.......
} publicStringgetScore(params){
//.......
} publicStringgetCasting(params){
//.......
} publicvoidopenMovieInfo(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:

importio.appium.java_client.pagefactory.Widget;
importorg.openqa.selenium.WebElement;
publicclassMovieextendsWidget{
protectedMovie(WebElementelement) {
super(element);
}
//convinient locator privateAndroidElementtitle;
//convinient locator privateAndroidElementscore;
//convinient locator privateAndroidElementcasting;
publicStringgetTitle(params){
//.......
} publicStringgetScore(params){
//.......
} publicStringgetCasting(params){
//.......
} publicvoidopenMovieInfo(params){
((AndroidElement) getWrappedElement()).tap(1, 1500);
} }

So, now page object looks

publicclassRottenTomatoesScreen {
@AndroidFindBy(alocatorwhichconvinienttofindasinglemovie-root - element)
privateList<Movie> movies;
//element declaration goes on publicStringgetMovieCount(){
returnmovies.size();
} publicMoviegetMovie(intindex){
//any interaction with sub-elements of a movie-element//will be performed outside of the page-object instance returnmovie.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(alocatorwhichconvinienttofindasinglemovie-root - element)
publicclassMovieextendsWidget{
...
}

and

publicclassRottenTomatoesScreen {
//!!! locator is not necessary at this caseprivateList<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 itpublicclassMoviesextendsWidget{
//with a custom locator or without itprivateList<Movie> movies;
...
}

and

publicclassRottenTomatoesScreen {
//with a custom locator or without itMoviesmovies;
...
}

Good! How to poputate all these fields?

As usual:

RottenTomatoesScreenscreen = newRottenTomatoesScreen();
PageFactory.initElements(newAppiumFieldDecorator(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(relevantlocator) //how to find a root elementpublicclassUsersWidgetextendsWidget{
@FindBy(relevantlocator) //this element will be found //using the root elementWebElementsubElement1;
@FindBy(relevantlocator) //this element will be found //using the root elementWebElementsubElement2;
@FindBy(relevantlocator) //a root element //of this widget is the sub-element which //will be found from top-elementUsersWidgetsubWidget;
//and so on.. 
}

and then it is enough

//above is the other field declarationUsersWidgetwidget;
//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(anotherrelevantlocator) //this locator overrides //the declared in the using classUsersWidgetwidget;
//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(relevantlocator) //how to find a root elementpublicclassUsersWidgetextendsWidget{
... }
//at this case the root element will be found by the locator//which is declared in superclasspublicclassUsersOverriddenWidgetextendsUsersWidget {
... }

and

@FindBy(relevantlocator2) //this locator overrides //all locators declared in superclassespublicclassUsersOverriddenWidget2extendsUsersWidget {
... }

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(relevantlocatorforbrowser/webviewhtmlorbydefault) @AndroidFindBy(relevantlocatorforAndroidUIautomator)
@iOSFindBy(relevantlocatorforAndroidUIautomator)
publicclassUsersWidgetextendsWidget {
@FindBy(relevantlocatorforbrowser/webviewhtmlorbydefault) @AndroidFindBy(relevantlocatorforAndroidUIautomator)
@iOSFindBy(relevantlocatorforiOSUIautomation)
RemoteWebElementsubElement1;
@FindBy(relevantlocatorforbrowser/webviewhtmlorbydefault) @AndroidFindBy(relevantlocatorforAndroidUIautomator)
@iOSFindBy(relevantlocatorforiOSUIautomation)
RemoteWebElementsubElement2;
@FindBy(relevantlocatorforbrowser/webviewhtmlorbydefault) //overrides a html/default//locator declared in the using class@AndroidFindBy(relevantlocatorforAndroidUIautomator) //overrides an Android UI automator//locator declared in the using class@iOSFindBy(relevantlocatorforiOSUIautomation) //overrides an iOS UI automation//locator declared in the using classUsersWidgetsubWidget;
//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*/classDefaultAbstractUsersWidgetextendsWidget{
}

and

@FindBy(locator)
publicclassUsersWidgetForHtmlextendsDefaultAbstractUsersWidget {
}

and

@AndroidFindBy(locator)
publicclassUsersWidgetForAndroidextendsDefaultAbstractUsersWidget {
}

and even

@iOSFindBy(locator)
publicclassUsersWidgetForIOSextendsDefaultAbstractUsersWidget {
}

and then

importio.appium.java_client.pagefactory.OverrideWidget;
...
//above is the other field declaration@OverrideWidget(html = UsersWidgetForHtml.class, androidUIAutomator = UsersWidgetForAndroid.class, iOSUIAutomation = UsersWidgetForIOS .class)
DefaultAbstractUsersWidgetwidget;
//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)
DefaultUsersWidgetwidget; //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
importio.appium.java_client.pagefactory.OverrideWidget;
...
//above is the other field declaration@OverrideWidget(html = UsersWidgetForHtml.class, androidUIAutomator = UsersWidgetForAndroid.class, iOSUIAutomation = UsersWidgetForIOS .class)
DefaultAbstractUsersWidgetwidget;
//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(WebElementelement) {
super(element);
}

@TikhomirovSergey

Copy link
Copy Markdown
ContributorAuthor

#228

@TikhomirovSergey

Copy link
Copy Markdown
ContributorAuthor

@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

Comment threadREADME.md

Copy link
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
ContributorAuthor

@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

@TikhomirovSergeyTikhomirovSergey mentioned this pull request Nov 13, 2015
@rompic

Copy link
Copy Markdown
Contributor

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

@rompic

Copy link
Copy Markdown
Contributor

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
Copy Markdown
ContributorAuthor

@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
Copy Markdown

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:

publicclassChatMessages {
@iOSFindBy(className = "UIATableCell")
privateList<Message> messageList;
}
publicclassMessageextendsWidget {
publicMessage(WebElementelement) {
super(element);
}
@iOSFindBy(className = "UIAStaticText")
publicIOSElementuserNameElement;
@iOSFindBy(className = "UIAStaticText")
publicIOSElementmessageElement;
}

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

@SButterfly

Copy link
Copy Markdown

Finally I did it.

publicclassMessageextendsWidget {
publicMessage(WebElementelement) {
super(element);
}
@iOSFindBy(uiAutomator = ".staticTexts()[0]")
privateIOSElementuserNameElement;
@iOSFindBy(uiAutomator = ".staticTexts()[1]")
privateIOSElementmessageElement;
}

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

@TikhomirovSergey

Copy link
Copy Markdown
ContributorAuthor

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 freeto 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

@TikhomirovSergey@bootstraponline@rompic@SButterfly