icon-arrow icon-check icon-mail icon-phone icon-facebook icon-linkedin icon-youtube icon-twitter icon-cheveron icon-download icon-instagram play close close icon-arrow-uturn icon-calendar icon-clock icon-search icon-chevron-process icon-skills icon-knowledge icon-kite icon-education icon-languages icon-tools icon-experience icon-coffee-cup
Werken bij Integration & Application Talents
Blog 05/11/2015

Sorting ADF tables using Selenium

Organize

Whitehorses
Mike Heeren /
Integratie expert

A couple of days ago I wrote my first blogpost about using Selenium for (unit)testing the user interface of an ADF application. This was more of a global blogpost about accessing ADF elements through Selenium. This time I am describing the solution of a more specific problem: Sorting ADF table components ascending or descending on a specific column.

The challenge we ran into, while writing our unittests, was that at one page of our application, we had to sort the contents of a table to be 100% sure whether a new record was added or not. Sorting a ADF table via the user interface is not really hard. When you have configured your table component that it can be sorted on specific columns, you hover the mouse cursor over the column header and two icons will appear. When you click one of these icons, the table will be sorted ascending or descending on that column.

Figure 1: Table column headers without focusing on any column.
Figure 1: Table column headers without focusing on any column.
Figure 2: Table column when mouse cursor is hovered above the “Name” column header.
Figure 2: Table column when mouse cursor is hovered above the “Name” column header.

The first part of this problem is, that the sorting icons are not available if the mouse cursor is not hovered above the column header. Therefor it is not possible to access the icon elements through the DOM.

Using the “Actions” class from Selenium, it is possible to perform mouse actions like moving to, and clicking on specific elements. I used the “Actions” class to:

  • First, move the mouse pointer to the column header. This adds the HTML table element recognized by the class “af_column_sort-indicator” to the user interface and the DOM. This HTML table contains the icons for sorting the column;
  • After this, we move the mouse pointer to the desired icon. The ascending icon can be recognized by the class “af_column_sort-ascending-icon-style”. The class used for the descending icon is “af_column_sort-descending-icon-style”;
  • Finally, we perform a mouse click. Because the mouse pointer is hovered “above” the icon at that moment, the icon will be clicked.

Another problem was, that ADF performs a lot of Partial Page Rendering (PPR) actions in our application. Therefor you are very likely to get a StaleElementReferenceException when accessing elements (such as the column headers, sort icons, etc.) in the unittests. Luckily Wilfred van der Deijl wrote a blogpost Selenium ADF Partial Page Rendering about how they tackled this issue for ADF 12c. Although we are testing a ADF 11g application, this blogpost was still very useful. We only had to do some minor modifications to the class to be able to use the same functionality on ADF 11g. The class given in the Red Heap blogpost uses the whyIsNotSynchronizedWithServer method from the AdfDhtmlPage class. This method is introduced in ADF 12c and not available in ADF 11g. However, the isSynchronizedWithServer method, which gives a boolean result whether the page is synchronized (PPR is done) or not, is already available in ADF 11g.

We use this isSynchronizedWithServer JavaScript method in the ClientSynchedWithServer class. This class is a implementation of an ExpectedCondition. Before we perform an action on a component, such as clicking on a button or editing an input field, we make the unittest wait until the page is completely synchronized with the server. We do this to avoid the StaleElementReferenceException. Using the code below we make the unittest wait up to 60 seconds, until the ClientSynchedWithServer.applymethod returns “true”.

new WebDriverWait(driver, 60).until(new ClientSynchedWithServer());

In the ClientSynchedWithServer.apply method we use the isSynchronizedWithServer JavaScript method to check if the client is already synchronized with the server. When this is the case within the provided 60 seconds, the unittest will move on to the next statement. When the ClientSynchronizedWithServer.apply method did not return “true” in the provided 60 seconds, a TimeoutException will be thrown.

The last problem that we ran into was that the unit test had to wait until the table sorting is finished. The first part was to make the page wait until the PPR is finished again. Here we were able to reuse the ClientSynchedWithServer condition again. But after the PPR is finished, we still want to check if the table is really sorted. I discovered that the HTML table element with the class “af_column_sort-indicator” inside the column header element, adds the “_afrsorted” attribute if a ADF table is sorted on that column. The value for this attribute is set to “1” if the table is sorted ascending on this column. If the table is sorted descending on this column, the value of the “_afrsorted­ attribute will be set to “2”.

To achieve the functionality of sorting ADF tables, I used 3 classes:

  • An extended version of the AdfComponentUtils You can read more about this class in my earlier blogpost: Testing ADF with Selenium: Accessing elements through multiple regions.
  • The Redheap ClientSynchedWithServer class with some minor modifications to support ADF 11g.
  • The WaitForTableSortedConditions class, which is a custom implementation of an ExpectedCondition. This class is used to make the unittest wait until the table is finished sorting.

Implemenation of AdfComponentUtils class:

package nl.whitehorses.selenium;
		 
import nl.whitehorses.selenium.conditions.ClientSynchedWithServer;
import nl.whitehorses.selenium.conditions.WaitForTableSortedCondition;
	 
import org.openqa.selenium.By;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.WebDriverWait;
		 
public class AdfComponentUtils {
		 
    public enum SortingOrder {
        ASCENDING,
        DESCENDING;
    }
		 
    public static final String ADF_UI_CLASS_COLUMN_SORT_INDICATOR = "af_column_sort-indicator";
    public static final String ADF_UI_CLASS_COLUMN_SORT_ASCENDING_ICON = "af_column_sort-ascending-icon-style";
    public static final String ADF_UI_CLASS_COLUMN_SORT_DESCENDING_ICON = "af_column_sort-descending-icon-style";
	 
    public static final String ADF_COLUMN_SORTED_ATTRIBUTE = "_afrsorted";
    public static final String ADF_COLUMN_SORTED_ASCENDING_VALUE = "1";
    public static final String ADF_COLUMN_SORTED_DESCENDING_VALUE = "2";
		 
    private static final long WAIT_FOR_CLIENT_SYNCHED_WITH_SERVER_TIMEOUT = 60;
    private static final long WAIT_FOR_TABLE_SORTED_TIMEOUT = 60;
		 
    private AdfComponentUtils () {
        super();
    }
		 
    public static void sortTableByColumn(WebDriver driver, SortingOrder sortingOrder, String columnId, String... parentIds) {
        String columnSorted = findWebElementByClass(driver, ADF_UI_CLASS_COLUMN_SORT_INDICATOR, concatElementIdWithParentIds(columnId, parentIds)).getAttribute(ADF_COLUMN_SORTED_ATTRIBUTE);
	 
        if (getAdfColumnAsendingValue(sortingOrder).equals(columnSorted)) {
            System.out.println("Table with column [" + columnId + "] already sorted [" + sortingOrder + "]");
        } else {
            Actions actions = new Actions(driver);
            actions.moveToElement(findWebElement(driver, columnId, parentIds));
            actions.moveToElement(findWebElementByClass(driver, getAdfUiClassColumnIcon(sortingOrder), concatElementIdWithParentIds(columnId, parentIds)));
            actions.click();
            actions.perform();
	 
            try {
                new WebDriverWait(driver, WAIT_FOR_TABLE_SORTED_TIMEOUT).until(new WaitForTableSortedCondition(sortingOrder, concatElementIdWithParentIds(columnId, parentIds)));
                System.out.println("Table with column [" + columnId + "] sorted [" + sortingOrder + "]");
            } catch (TimeoutException ex) {
                System.out.println("Failed to sort table with column [" + columnId + "] [" + sortingOrder + "]");
            }
        }
    }
		 
    public static void waitForClientSynchedWithServer(WebDriver driver) {
        new WebDriverWait(driver, WAIT_FOR_CLIENT_SYNCHED_WITH_SERVER_TIMEOUT).until(new ClientSynchedWithServer());
    }
		 
    public static WebElement findWebElement(WebDriver driver, String elementId, String... parentIds) {
        waitForClientSynchedWithServer(driver);
        return driver.findElement(By.xpath(constructFindElementXpath("@id", new StringBuilder(":").append(elementId).toString(), parentIds)));
    }
		 
    public static WebElement findWebElementByClass(WebDriver driver, String elementClass, String... parentIds) {
        waitForClientSynchedWithServer(driver);
        return driver.findElement(By.xpath(constructFindElementXpath("@class", elementClass, parentIds)));
    }
		 
    private static String constructFindElementXpath(String locatorType, String locatorValue, String... parentIds) {
        reverseStringArray(parentIds);
	 
        StringBuilder xpathBuilder = new StringBuilder();
	 
        if (parentIds != null) {
            for (String regionId : parentIds) {
                xpathBuilder.append("//*[").append(constructEndsWithXpath("@id", new StringBuilder("':").append(regionId).append("'").toString())).append("]");
            }
        }
	 
        xpathBuilder.append("//*[").append(constructEndsWithXpath(locatorType, new StringBuilder("'").append(locatorValue).append("'").toString())).append("]");
        return xpathBuilder.toString();
    }
		 
    private static String constructEndsWithXpath(String string, String suffix) {
        return new StringBuilder("substring(").append(string).append(", string-length(").append(string).append(") - string-length(").append(suffix).append(") + 1) = ").append(suffix).toString();
    }
		 
    private static void reverseStringArray(String... array) {
        for (int i = 0; i < array.length / 2; i++) {
            String temp = array[i];
            array[i] = array[array.length - 1 - i];
            array[array.length - 1 - i] = temp;
        }
    }
		 
    private static String[] concatElementIdWithParentIds(String elementId, String... parentIds) {
        String[] elementIds = new String[parentIds.length + 1];
        elementIds[0] = elementId;
	 
        for (int i = 0; i < parentIds.length; i++) {
            elementIds[i + 1] = parentIds[i];
        }
	 
        return elementIds;
    }
		 
    private static String getAdfUiClassColumnIcon(SortingOrder sortingOrder) {
        return sortingOrder == SortingOrder.ASCENDING ? ADF_UI_CLASS_COLUMN_SORT_ASCENDING_ICON : ADF_UI_CLASS_COLUMN_SORT_DESCENDING_ICON;
    }
		 
    private static String getAdfColumnAsendingValue(SortingOrder sortingOrder) {
        return sortingOrder == SortingOrder.ASCENDING ? ADF_COLUMN_SORTED_ASCENDING_VALUE : ADF_COLUMN_SORTED_DESCENDING_VALUE;
    }
}

Implementation of ClientSynchedWithServer class:

package nl.whitehorses.selenium.conditions;
		 
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.ExpectedCondition;
		 
public class ClientSynchedWithServer implements ExpectedCondition<Boolean> {
		 
    private static final String JS_SCRIPT = "return typeof AdfPage !== 'undefined' && typeof AdfPage.PAGE !== 'undefined' && typeof AdfPage.PAGE.isSynchronizedWithServer === 'function' && AdfPage.PAGE.isSynchronizedWithServer()";
		 
    public ClientSynchedWithServer() {
        super();
    }
		 
    @Override
    public Boolean apply(WebDriver driver) {
        JavascriptExecutor jsDriver = (JavascriptExecutor)driver;
        Object result = jsDriver.executeScript(JS_SCRIPT);
 
        return Boolean.TRUE.equals(result);
    }
}

Implementation of WaitForTableSortedCondition class:

package nl.whitehorses.selenium.conditions;
		 
import nl.whitehorses.selenium.AdfComponentUtils;
	 
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.ExpectedCondition;
		 
public class WaitForTableSortedCondition implements ExpectedCondition<Boolean> {
		 
    private String[] parentIds;
    private AdfComponentUtils.SortingOrder sortingOrder;
		 
    public WaitForTableSortedCondition(AdfComponentUtils.SortingOrder sortingOrder, String... parentIds) {
        this.sortingOrder = sortingOrder;
        this.parentIds = parentIds;
    }
		 
    @Override
    public Boolean apply(WebDriver webDriver) {
        String expectedVal = sortingOrder == AdfComponentUtils.SortingOrder.DESCENDING ? AdfComponentUtils.ADF_COLUMN_SORTED_DESCENDING_VALUE : AdfComponentUtils.ADF_COLUMN_SORTED_ASCENDING_VALUE;
	 
        AdfComponentUtils.waitForClientSynchedWithServer(webDriver);
        return expectedVal.equals(AdfComponentUtils.findWebElementByClass(webDriver, AdfComponentUtils.ADF_UI_CLASS_COLUMN_SORT_INDICATOR, parentIds).getAttribute(AdfComponentUtils.ADF_COLUMN_SORTED_ATTRIBUTE));
    }
}

Good luck!

 

Source(s): RedHeap blog by Wilfred van der Deijl: Waiting for Oracle ADF Partial Page Rendering in Selenium tests

Geen reacties

Geef jouw mening

Reactie plaatsen

Reactie toevoegen

Jouw e-mailadres wordt niet openbaar gemaakt.

Geen HTML

  • Geen HTML toegestaan.
  • Regels en alinea's worden automatisch gesplitst.
  • Web- en e-mailadressen worden automatisch naar links omgezet.
Whitehorses
Mike Heeren /
Integratie expert

Wil je deel uitmaken van een groep gedreven en ambitieuze experts? Stuur ons jouw cv!