JBoss Community Archive (Read Only)

Graphene 2

Page Fragments

The concept of Page Fragments and motivation behind them were already introduced in Page Abstractions. Here you can find more detailed info about:

  1. Relation to Page Objects

  2. Usage

  3. Creating Page Fragments

  4. Samples of Page Fragments

    1. Autocomplete

    2. Calendar

    3. Table

    4. Login component

Relation to Page Objects

Page Fragments and Page Objects are similar concepts. They both decouples HTML structure of the tested application from the tests. They both encapsulate some kind of page services or the interactions a user can do with the page.

The difference is that Page Objects are encapsulating a specific page (or its part) of specific application, while Page Fragments are dealing with parts of the page, its widgets, components, basically fragments which are reusable across the whole web UI framework in which the application under test is written.

Usage

To use Page Fragment in your tests only thing you need to do is use an implementation class of the Page Fragment together with @FindBy annotation pointing to the root of the fragment.

DeclaringPageFragmentInTest.java
@FindBy(css="cssLocatorOfThePageFragmentRoot")
private PageFragmentImplementation pageFragment;

Use @FindBy annotation as you are used to with plain WebElement, so indeed you can use other locating strategies (xpath, id, className, ...) to reference the root of the Page fragment. The root of the fragment is DOM element which is parent for all other elements which are part of the Page fragment and need to be referenced from it (e.g. Calendar and its cells).

These Page fragments and other WebElement fields are dynamically initialized by Graphene so no need to initialize them via factories.

Handy Hints
  • You can declare Page Fragments in the above mentioned way either directly in your tests or you can declare them in the same way within your Page Objects.

  • You can nest your Page Fragments in other Page Fragments and create thus nice structure of your fragments (e.g. Page Fragment application menu -> nested menu group -> nested menu item).

  • Page Fragments can be declared as nested classes, however, to better reuse them across your test suites, it is not recommended.

There is another way to create Page Fragments. You can create them dynamically with use of Graphene.createPageFragment(Class<T> clazz, WebElement root). This may be particularly useful when implementing Page Fragment for e.g. Tab Panel widget.

Creating Page Fragments

To define own Page Fragment one needs to:

  1. Recognize a fragment of the page which can be abstracted and encapsulated into a Page Fragment (Basically web UI components which are rendered always with the same DOM structure.)

  2. Create a class or if you find it appropriate also an interface for it.

  3. Define the parts you need for implementing the fragment's services, which will be referenced from the injected root. Annotate them with @FindBy annotation.

  4. Define the methods which are basically encapsulation of the fragment services (Calendar services - get date, set date, ...).

    If you need to access the injected root of your Page Fragment, you ca do so by declaring a WebElement field with a @Root annotation.

    There are two packages from where you can import @FindBy annotation:

    1. org.openqa.selenium.support.FindBy - well know @FindBy from Selenium 2

    2. org.jboss.arquillian.graphene.enricher.findby.FindBy - Graphene FindBy annotation which adds on top of classic @FindBy many useful features

So the implementation can look like snippet below.

PageFragmentExample.java
import org.jboss.arquillian.graphene.spi.annotations.Root;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;

public class PageFragmentExample {

    @Root
    private WebElement optionalRoot;

    @Drone
    private WebDriver browser;

    @FindBy(css="relativeLocatorOfThisPageFragmentPart")
    private WebElement otherPageFragmentPart;

    @FindBy(xpath="relativeLocatorOfThisPageFragmentPart")
    private WebElement alsoPageFragmentPart;

    public void firstServiceEncapsulated() {
        otherPageFragmentPart.click();
    }

    public void secondServciceEncapsulated() {
        alsoPageFragmentPart.clear();
    }

    public void thirdServiceWhichNeedsDirectAccessToRoot() {
        root.click();
    }

    public void fourthServiceWhichNeedsBrowserInstance() {
        Actions builder = new Actions(browser);

        builder.keyDown(Keys.CONTROL)
               .click(otherPageFragmentPart)
               .keyUp(Keys.CONTROL);
        builder.build().perform();
    }

    //other services and help methods
}
Be Careful

with the xpath locators. They can be written in a way that it does not matter from which element they are referenced. Prefer therefore locating according to id, css or class name.

Handy Hint

Indeed you will need a reference for WebDriver instance. You can easily inject it with @Drone annotation.

Note that you have to use private fields for all Graphene initialized WebElement/Page Fragments etc. Use their getters from tests.

Page fragments in Multiple Browsers scenario

If you use page abstractions together with parallel browsers feature, be sure to check Using Page Abstractions with Multiple Browsers.

Samples of Page Fragments

Let's consider following code snippets, which are reflecting some examples of the Page Fragments to start with. In each example you can find:

  • An image of the page fragment to be clear what we are speaking about.

  • HTML structure of the page fragment rendered in RichFaces framework.

  • Example of the Page Fragment encapsulation, either the implementation or just the interface for it.

Autocomplete

images/author/download/attachments/53117994/autocomplete.png

images/author/download/attachments/53117994/autocompleteHTML.png

AutocompletePageFragment.java
public class AutocompletePageFragment<T> {

   	@Root
    WebElement root;

    @FindBy(css = CSS_INPUT)
    WebElement inputToWrite;

    public static final String CLASS_NAME_SUGGESTION_LIST = "rf-au-lst-cord";
    public static final String CLASS_NAME_SUGGESTION = "rf-au-itm";
    public static final String CSS_INPUT = "input[type='text']";

    private String separator = " ";

    public boolean areSuggestionsAvailable() {

        List<WebElement> suggestionLists = root.findElements(
              By.xpath("//*[contains(@class,'" + CLASS_NAME_SUGGESTION_LIST + "')]"));

        WebElement suggList = getRightSuggestionList();

        return suggList.isDisplayed();
    }

    public void finish() {
        inputToWrite.sendKeys(" ");
        inputToWrite.sendKeys("\b\b");
        root.click();
    }

    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 void setSeparator(String regex) {
        this.separator = regex;
    }

    public void type(String value) {
        inputToWrite.sendKeys(value);
        try {
            waitForSuggestions(GUI_WAIT);
        } catch (TimeoutException ex) {
            // no suggestions available

        }
    }

    public List<Suggestion<T>> type(String value, SuggestionParser<T> parser) {
        List<Suggestion<T>> suggestions = new ArrayList<Suggestion<T>>();

        inputToWrite.sendKeys(value);
        try {
            waitForSuggestions(GUI_WAIT);
        } catch (TimeoutException ex) {
            // no suggestions available
            return suggestions;
        }

        suggestions = getAllSuggestions(parser);
        return suggestions;
    }

    private void waitForSuggestions(int timeout) {
        (new WebDriverWait(GrapheneContext.getProxy(), timeout))
           .until(new ExpectedCondition<Boolean>() {

            public Boolean apply(WebDriver d) {
                return areSuggestionsAvailable();
            }
        });
    }

    //other Autocomplete services and help methods
}

Calendar

images/author/download/attachments/53117994/calendar.png

images/author/download/attachments/53117994/calendarHTML.png

CalendarPageFragmentImpl.java
public class CalendarPageFragmentImpl {

	@Root
	private WebElement root;

	@FindBy(className = "rf-cal-inp")
	private WebElement input;

	@FindBy(css = "td[class=\"rf-cal-hdr-month\"] > div")
	private WebElement showYearAndMonthEditorButton;

	@FindBy(css = "img:nth-of-type(1)")
	private WebElement showCalendarButton;

	@FindBy(className = "rf-cal-day-lbl")
	private WebElement popupWithCalendar;

	@FindBy(css = "div[class=\"rf-cal-time-btn\"]:nth-of-type(1)")
	private WebElement okButton;

	@FindBy(css = "table[class=\"rf-cal-monthpicker-cnt\"] td:nth-of-type(4) > div")
	private WebElement nextDecade;

	@FindBy(css = "table[class=\"rf-cal-monthpicker-cnt\"] td:nth-of-type(3) > div")
	private WebElement previousDecade;

	private final String YEAR_AND_MONTH_LOCATOR_CSS =
                                                       "div[class=\"rf-cal-edtr-btn\"]";

	private final String DAY_LOCATOR_CLASS = "rf-cal-c";

	/**
	 * The format of date displayed on the calendar input, default dd/M/yyhh:mma
	 */
	private String dateFormat = "dd/M/yy hh:mm a";

	public void showCalendar() {

		if (!popupWithCalendar.isDisplayed()) {
			showCalendarButton.click();

			waitUntilPopupWithCalendarIsDisplayed();
		}
	}

	private void gotoDate(Date date) {
		showCalendar();

		Calendar cal = new GregorianCalendar();
		cal.setTime(date);
		int wishedYear = cal.get(Calendar.YEAR);
		// month is indexed from 0!
		int wishedMonth = cal.get(Calendar.MONTH);
		int wishedDay = cal.get(Calendar.DAY_OF_MONTH);

		cal.setTime(new Date(System.currentTimeMillis()));

		int todayYear = cal.get(Calendar.YEAR);
		int todayMonth = cal.get(Calendar.MONTH);
		// int todayDay = cal.get(Calendar.DAY_OF_MONTH);

		showYearAndMonthEditorButton.click();

		if ((wishedYear != todayYear) || (wishedMonth != todayMonth)) {
			List<WebElement> years;
			String txt;

			if (todayYear > wishedYear) {
				int howManyDecadesLessOrMore =
                                              (todayYear - wishedYear) / 10;

				for (int i = 0; i < howManyDecadesLessOrMore; i++)
					previousDecade.click();
			}

			if (todayYear < wishedYear) {
				int howManyDecadesLessOrMore =
                                              (wishedYear - todayYear) / 10;

				for (int i = 0; i < howManyDecadesLessOrMore; i++)
					nextDecade.click();
			}

			selectYear(wishedYear);

			years = root.findElements(By
					.cssSelector(YEAR_AND_MONTH_LOCATOR_CSS));

			for (WebElement i : years) {
				txt = i.getText().trim();

				if (txt.matches("[a-zA-Z]+?")) {
					if (txt.equals("Jan") && wishedMonth == 0) {
						i.click();
						// break;
					} else if(txt.equals("Feb") && wishedMonth == 1) {
						i.click();
						// break;
					} else if (txt.equals("Mar") && wishedMonth == 2){
						i.click();
						// break;
					} else if (txt.equals("Apr") && wishedMonth == 3){
						i.click();
						// break;
					} else if (txt.equals("May") && wishedMonth == 4){
						i.click();
						// break;
					} else if (txt.equals("Jun") && wishedMonth == 5){
						i.click();
						// break;
					} else if (txt.equals("Jul") && wishedMonth == 6){
						i.click();
						// break;
					} else if (txt.equals("Aug") && wishedMonth == 7){
						i.click();
						// break;
					} else if (txt.equals("Sep") && wishedMonth == 8){
						i.click();
						// break;
					} else if (txt.equals("Oct") && wishedMonth == 9){
						i.click();
						// break;
					} else if (txt.equals("Nov") && wishedMonth == 10{
						i.click();
						// break;
					} else if (txt.equals("Dec") && wishedMonth == 11{
						i.click();
						// break;
					}
				}
			}

			okButton.click();
		}

		List<WebElement> days = root.findElements(By
				.className(DAY_LOCATOR_CLASS));
		String txt;
		for (WebElement i : days) {
			txt = i.getText().trim();
			int day = new Integer(txt);
			if (day == wishedDay) {
				i.click();
				break;
			}
		}
	}

	/**
	 * Selects the year on the calendar, note that the month and year editor has
	 * to be shown already
	 *
	 * @param wishedYear
	 *            the year you want to set
	 * @return true if the year was successfully set, false otherwise
	 */
	private boolean selectYear(int wishedYear) {
		List<WebElement> years = root.findElements(By
				.cssSelector(YEAR_AND_MONTH_LOCATOR_CSS));
		String txt;

		for (WebElement i : years) {

			txt = i.getText().trim();
			int year;

			if (txt.matches("\\d+?")) {
				year = new Integer(txt);

				if (wishedYear == year) {
					i.click();
					return true;
				}
			}
		}
		return false;
	}

	public void gotoDateTime(DateTime dateTime) {
		Date date = dateTime.toDate();
		gotoDate(date);
	}

	public void gotoDateTime(DateTime dateTime, ScrollingType type) {
		throw new UnsupportedOperationException("Not implemented yet!");
	}

	public CalendarDay gotoNextDay() {
		Date date = getDateTime().toDate();
		Calendar cal = new GregorianCalendar();
		cal.setTime(date);
		cal.roll(Calendar.DAY_OF_MONTH, true);

		gotoDate(cal.getTime());

		// CalendarDay day = new CalendarDayImpl();
		return null;
	}
}

Table

images/author/download/attachments/53117994/table.png

images/author/download/attachments/53117994/tableHTML.png

Table.java
public interface TableComponent {

    /**
     * Associates this data table with a given data scroller
     *
     * @param scroller the scroller to associate this table with
     */
    void setDateScroller(DataScrollerComponent scroller);

    /**
     * <p>
     * Returns the total number of rows in this particular table.
     * </p>
     * <p>
     * The <code>rowspan</code> html atribute is not considered,
     * in other words the row with <code>rowspan</code> equals 2 is
     * considered as one row.
     * </p>
     *
     * @return
     */
    int getNumberOfRows();

    /**
     * <p>
     * Returns total number of cells in this particular table.
     * </p>
     *
     * @return
     */
    int getNumberOfCells();

    <T> List<Cell<T>> findCells(CellFunction<T> function);

    List<Row> findRow(RowFunction function);

    <T> List<Column<T>> findColumns(ColumnFunction<T> function);

    /**
     * <p>
     * Returns the total number of columns in this particular table.
     * </p>
     * <p>
     * The <code>colspan</code> html atribute is not considered,
     * in other words the column with <code>colspan</code> equals 2 is
     * considered as one column.
     * </p>
     *
     * @return
     */
    int getNumberOfColumns();

    /**
     * Returns the particular cell, the cell with coordinations determined
     * by given row and column.
     *
     * @param row
     * @param column
     * @return
     */
    <T> Cell<T> getCell(Row row, Column<T> column);

    /**
     * Returns the list of all header cells, in other words the whole table header.
     *
     * @return
     */
    Header getTableHeader();

    /**
     * Returns the list of all footer cells, in other words the whole table footer.
     *
     * @return
     */
    Footer getTableFooter();

    /**
     *
     * @return
     */
    List<Row> getAllRows();

    /**
     * Returns the first row of the table, the header row if available, is not counted.
     *
     * @return
     */
    Row getFirstRow();

    /**
     * Returns the last row of the table, the footer row if available, is not counted.
     *
     * @return
     */
    Row getLastRow();

    /**
     * <p>
     * Returns the row with the order determined by given param <code>order</code>.
     * </p>
     * <p>
     * Rows are indexed from 0. The header row if available is not counted.
     * </p>
     *
     * @param order the order of the row
     * @return the particular row, or null if it does not exist
     */
    Row getRow(int order);
}

Login component

LoginPageFragment.java
public class LoginPageFragment {

    @Root
    private WebElement root;

    @FindBy(css="input[type='text']")
    private WebElement loginInput;

    @FindBy(css="input[type='password']")
    private WebElement passwordInput;

    @FindBy(css="input[type='submit']")
    private WebElement loginButton;

    @FindBy(className="errorMsg")
    private WebElement errorMsg;

    public void fillInLogin(String login) {
        loginInput.sendKeys(login);
    }

    public void fillInPassword(String password) {
        passwordInput.sendKeys(password);
    }

    public void login() {
        loginButton.click();
    }

    public String getErrorMsg() {
        return errorMsg.getText();
    }
}
JBoss.org Content Archive (Read Only), exported from JBoss Community Documentation Editor at 2020-03-10 12:14:40 UTC, last content change 2013-10-10 10:09:41 UTC.