Introducing Arquillian Graphene Page Fragments
Tools like Arquillian Graphene, WebDriver, or just plain Selenium combined with concepts like Page Objects can help you automate your functional(integration, whatever…) web UI tests. We have already introduced some of the advances brought to you by Arquillian Graphene earlier. In this blog entry we will look a bit deeper into a new concept introduced in Arquillian Graphene 2.0.0.Alpha2; Page Fragments.
Consider the following example taken from the RichFaces component showcase. The page under test contain three Autocomplete widgets.
public class TestAutocompleteWidgets extends AbstractGrapheneTest {
private JQueryLocator minCharInput = jq("input[type=text]:eq(0)"); private JQueryLocator multipleSelectionInput = jq("input[type=text]:eq(1)"); private JQueryLocator selectFirstFalseInput = jq("input[type=text]:eq(2)"); private JQueryLocator selection = jq("div.rf-au-itm");
@Test public void testFirstAutocomplete() {
graphene.keyPress(minCharInput, 'a');
assertFalse(graphene.isElementPresent(selection), "The suggestion list should not be visible, since there is only one char!");
String keys = "ar"; graphene.focus(minCharInput); selenium.type(minCharInput, keys); guardXhr(graphene).fireEvent(minCharInput, Event.KEYPRESS);
assertTrue(graphene.isVisible(selection), "The suggestion list should be visible, since there are two chars!");
String actualArizona = graphene.getText(jq(selection.getRawLocator() + ":eq(0)")); assertEquals(actualArizona, "Arizona", "The provided suggestion should be Arizona!");
String actualArkansas = graphene.getText(jq(selection.getRawLocator() + ":eq(1)")); assertEquals(actualArkansas, "Arkansas", "The provided suggestion should be Arkansas!");
}
@Test public void testSecondAutocomplete() {
char key = 'a'; selenium.focus(multipleSelectionInput); guardXhr(selenium).keyPress(multipleSelectionInput, key);
assertTrue(selenium.isVisible(selection), "The suggestion list should be visible, since there is correct starting char!");
selenium.keyPressNative(KeyEvent.VK_ENTER);
key = ' '; selenium.keyPress(multipleSelectionInput, key);
key = 'w';
selenium.focus(multipleSelectionInput); guardXhr(selenium).keyPress(multipleSelectionInput, key);
assertTrue(selenium.isVisible(selection), "The suggestion list should be visible, since there is correct starting char!");
selenium.keyPressNative(KeyEvent.VK_ENTER);
String actualContentOfInput = selenium.getValue(multipleSelectionInput); assertEquals(actualContentOfInput, "Alabama Washington", "The input should contain something else!"); }
@Test public void testThirdAutocomplete() { //similar autocomplete interactions as in the previous tests } }
Now, ask yourself the following questions:
- Do you find these types of tests less robust than for example unit tests for the persistence layer?
- Do you think these tests still pass when the underlying HTML change?
- Do you see repeating code in the these tests?
In my opinion you should have a clean answer to all of these questions. You are probably aware that tests should be loosely coupled with the underlying HTML structure of the application under test as it makes tests more robust and changes in the HTML structure of the page will not directly affect all tests.
Let’s apply the Page Objects pattern to split the HTML structure and the test logic. The Page Object will encapsulate the HTML structure so when the HTML change, only your Page Objects has to change.
- But what about when I’m testing another application that use the same Autocomplete widget?
- Should I copy the part of the Page Object that interact with the Autocomplete widget and paste it around in my code?
But as you’re already thinking: this would be a major don’t-repeat-yourself violation! Is there something we could do to improve this?
Yes, there is! Arquillian Graphene Page Fragments to the rescue!
Page Fragments, what are they?
- Page Fragments are any repeating part of a page, any widget, web component, etc.
- They encapsulate parts of the page into reusable test components across your whole test suite.
- You can differentiate each fragment by its root element and reference other elements as children of that root.
- They leverage Selenium WebDriver under the hood combined with all of the killer features of Graphene.
- And they come with a set of utilities which simplify using them within tests and Page Objects.
How to define Page Fragments
public class AutocompleteFragment<T> {
@Root WebElement root;
@FindBy(css = "input[type='text']") WebElement inputToWrite;
public static final String CLASS_NAME_SUGGESTION = "rf-au-itm";
public List<Suggestion<T>> getAllSuggestions(SuggestionParser<T> parser) { List<Suggestion<T>> allSugg = new ArrayList<Suggestion<T>>();
if (areSuggestionsAvailable()) { WebElement rightSuggList = getRightSuggestionList(); List<WebElement> suggestions = rightSuggList.findElements( By.className(CLASS_NAME_SUGGESTION));
for (WebElement suggestion : suggestions) { allSugg.add(parser.parse(suggestion)); } }
return allSugg; }
public List<Suggestion<T>> type(String value, SuggestionParser<T> parser) { List<Suggestion<T>> suggestions = new ArrayList<Suggestion<T>>();
inputToWrite.sendKeys(value); try { waitForSuggestions(); } catch (TimeoutException ex) { // no suggestions available return suggestions; }
suggestions = getAllSuggestions(parser); return suggestions; }
//other handy encapsulation of Autocomplete services }
The example is just a snippet from the full implementation of the RichFaces Autocomplete component.
Notice the @Root
annotation? The value of the root field is automatically injected by Graphene. The root
field contain the root element as defined by the @FindBy
annotation on the injection point of the Page Fragment in the Page Object or test. All @FindBy
fields in the Page Fragment will use this root as a starting point.
The fragment implementation is pretty generic and therefore reusable in all tests in all applications that use the Autocomplete widget. A full implementation would encapsulate all the services this fragment provide, but it could for example also encapsulate browser specific interactions like submit or click actions. There are no boundaries!
Using Page Fragments and Page Objects together
Let’s rewrite the previous test to use our new Page Fragment together with our Page Object.
First we define a Page Object which contain the structure of the page under test.
public class TestPage {
@FindBy(css = "div.rf-au:nth-of-type(1)") private AutocompleteFragment<String> autocomplete1;
@FindBy(css = "div.rf-au:nth-of-type(2)") private AutocompleteFragment<String> autocomplete2;
@FindBy(css = "div.rf-au:nth-of-type(3)") private AutocompleteFragment<String> autocomplete3;
@FindBy(xpath = "//*[contains(@id,'sh1')]") private WebElement viewSourceLink;
//all getters for the fields
//other handy methods which you find useful when testing those three Autocomplete widgets }
Declaring the Page Fragment in the Page Object is the preferred option, but you can also declare the Page Fragment directly in the test if desired. The only thing you need to do is to annotate the Page Fragment object with WebDriver’s @FindBy
annotation to refer to the fragment’s root DOM element. That simple!
Graphene differentiates between Page Fragments and plain WebElements in the same Page Object so they can be declared side by side. All of the initialization is done automatically, so there is no need to initialize the Page Fragments or Page Objects in the @Before
method of your test case.
In this last example we see how the autocomplete widgets’s Page Fragment is used in the test case.
public class TestWhichUsesPageFragments extend AbstractTest {
@Page private TestPage testPage;
@Test public void testFirstAutocomplete { AutocompleteFragment<String> autocomplete = testPage.getAutocomplete1();
autocomplete.type("a");
assertFalse(autocomplete.areSuggestionsAvailable(), "The suggestion list should not be visible, since there is only one char!");
String keys = "ar"; autocomplete.type(keys);
assertTrue(autocomplete.areSuggestionsAvailable(), "The suggestion list should be visible, since there are two chars!");
List<Suggestion<String>> expectedSuggestions = new ArrayList<Suggestion<String>>(); expectedSuggestions.add(new Suggestion<String>("Arizona")); expectedSuggestions.add(new Suggestion<String>("Arkansas"));
assertEquals(autocomplete.getAllSuggestions(new StringSuggestionParser()), expectedSuggestions, "Suggestions are wrong!"); }
@Test public void testSecondAutocomplete() { AutocompleteFragment<String> autocomplete = testPage.getAutocomplete2();
String key = "a"; autocomplete.type(key);
String errorMsg = "The suggestion list should be visible, since there was typed correct char "; assertTrue(autocomplete.areSuggestionsAvailable(), errorMsg + key);
autocomplete.autocomplete(autocomplete.getFirstSuggestion());
autocomplete.type(" ");
key = "w" autocomplete.type(key);
assertTrue(autocomplete.areSuggestionsAvailable(), errorMsg + key);
autocomplete.autocomplete(autocomplete.getFirstSuggestion());
String actualContentOfInput = autocomplete.getInputValue(); assertEquals(actualContentOfInput, "Alabama Washington", "The input should contain something else!"); }
@Test public void testThirdAutocomplete() { //similar autocomplete interactions as in the previous tests } }
As your application grow, the only thing that needs to change is the root references of your Page Fragments. Last but not least you will be able to make your Page Fragment availabe to be used by other tests in other applications.
To try Page Fragments check out the Graphene 2.0.0.Alpha2 release. Getting start information can be found in the Reference Documentation.