Test your ICN plug-in’s UI with Selenium

Lately I’ve been writing about test, and to finish this series of post, after unit tests and integration tests, I would like to talk about UI test. UI is not easy to test, because it requires a lot of user interactions. This is really important though, because UI tests make sure the final result, i.e. what the user sees, is what we want him to see. And as we all know, users remember mostly the UI defects 🙂

Update: I actually wrote an introduction on how to use Selenium, this post is meant more to give example of uses of Selenium applied to ICN and indirectly Dojo.

Not long ago, to test web application, we were writing test cases and you or any other unlucky tester was validating all of them one by one, and it was taking ages. Thanks god, there is nowadays better solutions, meaning lazy automatic solution. I will introduce in this post how to use a Web Browser automation tool to test ICN and your plug-in. We will use Selenium, which I think is a great tool. This will be applied to ICN, you can find plenty of Selenium tutorials on the web anyway, We will see how to automate basic action, like login, logout, create a folder, a document, file a document, accept message dialog. Then it will be up to you to adapt all this to your plug-ins.

Here is a quick video to give you an idea of what can be done with Selenium and ICN.

Writing browser automation with ICN is not easy, because there is not fixed ID, thanks to dojo and how ICN is designed. That’s why we will use intensively XPath queries to find elements in the page, so you might want to take a look to the XPath syntax if you are not familiar with it!

Also I recommend you to read this great article first, to be introduced to Selenium. It will ease a lot the comprehension of the following examples.

Understand the global structure

The structure of ICN and Dojo is sometimes strange, I guess some choices have been made for performance reasons. Often, stacks are used or element are hidden, which means that if you are waiting for an element to be absent of your page, you’re test will certainly fail because it will stay on the page hidden. For instance, the main page contains the login page and the main desktop, as a stack, and only one of both is visible at one moment. This makes checking if we are on the login page or if we are logged in not easy since it is the same page. The main desktop contains all features also as tabs, and so on.

Examples

How I write to make new ICN features available in my tests is quite easy. I always start by writing a Container class to target element from the page, the dialog or a specific widget. Then I write a view, which is more about action user would like to do. View are a higher level of abstraction and method are actually basic action like open this menu, click this button, logout

Therefore all examples given here are composed of the Container and the view. The purpose of this post is not to explain Selenium, since here are great tutorials on the web for that, but it is meant to give you a head start on applying selenium to ICN. That’s why there is not much explanation, but more example so you can see how to target element in ICN via XPath, and to basic things like waiting for some part of the page to load.

Log in

Login Container

public class LoginContainer {

    private static final String visibleTab = "//div[@id='ECMWebUI']/div[contains(@class, 'ecmLayout')]/div/div[contains(@class, 'dijitStackContainer')]/div[contains(@class, 'dijitVisible') and ./div[contains(@class, 'ecmLoginPane')]]";

    @FindBy(how = How.XPATH, using = visibleTab)
    public WebElement loginPageTab;

    @FindBy(how = How.XPATH, using = visibleTab + "//div[@class = 'field' and contains(label/text(),'User name')]/div[contains(@class, 'dijitTextBox')]/div[contains(@class, 'dijitInputField')]/input")
    public WebElement usernameInput;

    @FindBy(how = How.XPATH, using = visibleTab + "//div[@class = 'field' and contains(label/text(),'Password')]/div[contains(@class, 'dijitTextBox')]/div[contains(@class, 'dijitInputField')]/input")
    public WebElement passwordInput;

    @FindBy(how = How.XPATH, using = visibleTab + "//span[contains(@class, 'dijitButton')]//span[contains(@class, 'dijitButtonText') and contains(text(), 'Log In')]")
    public WebElement submitButton;

    @FindBy(how = How.CSS, using = ".inlineMessageError")
    public WebElement errorMessage;

}

Then the View class:

public class LoginView {
	private static final LoginContainer loginContainer = PageFactory.initElements(BrowserDriver.getCurrentDriver(), LoginContainer.class);

	public static void isDisplayedCheck(){
	    System.out.println("Checking login page is displayed");
		BrowserDriver.waitForElement(loginContainer.loginPageTab);
		BrowserDriver.waitForElement(loginContainer.usernameInput);
	}

	public static void login(String username, String password){
		System.out.println("Logging in with username:" + username + " password: ******");
		loginContainer.usernameInput.clear();
		loginContainer.usernameInput.sendKeys(username);
		loginContainer.passwordInput.sendKeys(password);
		loginContainer.submitButton.click();
		System.out.println("Login submitted");
	}

	public static void checkLoginSuccess() throws Exception{
		System.out.println("Check login was successful");
		HomeView.isDisplayedCheck();
	}

	public static void checkLoginErrors(){
		System.out.println("Check login errors displayed");
		BrowserDriver.waitForElement(loginContainer.errorMessage);
	}
}

And you will need to see my BrowserDriver class, which gather also some common actions to wait element and class/style on element:

public class BrowserDriver {

    private static WebDriver mDriver;

    public synchronized static WebDriver getCurrentDriver() {
        if (mDriver == null) {
            try {

                String browser = System.getProperty("browser");

                if ("firefox".equals(browser)) {
                    System.setProperty("webdriver.firefox.bin", "C:\\Program Files (x86)\\Mozilla Firefox 27\\firefox.exe");
                    System.setProperty("webdriver.firefox.profile", "firefox27");
                    mDriver = new FirefoxDriver(getFirefoxProfile());
                } else {
                    System.setProperty("webdriver.chrome.driver", "test/chromedriver.exe");
                    mDriver = new ChromeDriver();
                }
                mDriver.manage().timeouts().implicitlyWait(2, TimeUnit.SECONDS);
                mDriver.manage().timeouts().pageLoadTimeout(120, TimeUnit.SECONDS);
                mDriver.manage().timeouts().setScriptTimeout(60, TimeUnit.SECONDS);
            } finally {
                Runtime.getRuntime().addShutdownHook(new Thread(new BrowserCleanup()));
            }
        }
        return mDriver;
    }

    private static FirefoxProfile getFirefoxProfile() {
        FirefoxProfile firefoxProfile = new FirefoxProfile();
        try {
            firefoxProfile.addExtension(new File("firefox-plugins\\firebug-1.12.8.xpi"));
            firefoxProfile.addExtension(new File("firefox-plugins\\firefinder-1.4.xpi"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        firefoxProfile.setPreference("extensions.firebug.currentVersion", "1.12.8");  // Avoid startup screen
        return firefoxProfile;
    }

    private static class BrowserCleanup implements Runnable {
        public void run() {
            System.out.println("Closing the browser");
            close();
        }
    }

    public static void close() {
        try {
            getCurrentDriver().quit();
            mDriver = null;
            System.out.println("closing the browser");
        } catch (UnreachableBrowserException e) {
            System.out.println("cannot close browser: unreachable browser");
        }
    }

    public static void loadPage(String url) {
        ;
        System.out.println("Directing browser to:" + url);
        getCurrentDriver().get(url);
    }

    public static WebElement waitForElement(WebElement elementToWaitFor) {
        WebDriverWait wait = new WebDriverWait(getCurrentDriver(), Constants.TIMEOUT);
        return wait.until(ExpectedConditions.visibilityOf(elementToWaitFor));
    }

    public static WebElement waitClickable(WebElement elementToWaitFor) {
        WebDriverWait wait = new WebDriverWait(getCurrentDriver(), Constants.TIMEOUT);
        return wait.until(ExpectedConditions.elementToBeClickable(elementToWaitFor));
    }

    public static void waitDojoReady() {
        final JavascriptExecutor exec = (JavascriptExecutor) getCurrentDriver();
        final String v = "Test_" + System.currentTimeMillis();
        exec.executeScript(v + "=false;");
        exec.executeScript("require([\"dojo/domReady!\"], function(){" + v + "=true});");
        WebDriverWait wait = new WebDriverWait(mDriver, Constants.TIMEOUT);
        wait.until(new Function<Object, Boolean>() {
            @Override
            public Boolean apply(Object arg0) {
                return (Boolean) exec.executeScript("return " + v + ";");
            }
        });
    }

    public static void waitEcmWait() {
        WebElement body = getCurrentDriver().findElement(By.cssSelector("body"));
        BrowserDriver.waitForCssClassNotPresent(body, "ecmWait");
    }

    public static void waitICNDesktopConnected() {
        final JavascriptExecutor exec = (JavascriptExecutor) getCurrentDriver();
        WebDriverWait wait = new WebDriverWait(mDriver, Constants.TIMEOUT);
        wait.until(new Function<Object, Boolean>() {
            @Override
            public Boolean apply(Object arg0) {
                return (Boolean) exec.executeScript("return ecm.model.desktop.connected;");
            }
        });
    }

    public static void deleteAllCookies() {
        getCurrentDriver().manage().deleteAllCookies();
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    public static void waitForCssClass(final WebElement e, final String cssClass) {
        WebDriverWait wait = new WebDriverWait(getCurrentDriver(), Constants.TIMEOUT);
        wait.until(new Function() {
            @Override
            public Object apply(Object arg0) {
                System.out.println(e.getAttribute("class"));
                return e.getAttribute("class").matches(".*\\b" + cssClass + "\\b.*");
            }
        });
    }

    public static void waitForCssClassNotPresent(final WebElement e, final String cssClass) {
        WebDriverWait wait = new WebDriverWait(getCurrentDriver(), Constants.TIMEOUT);
        wait.until(new Function<Object, Boolean>() {
            @Override
            public Boolean apply(Object arg0) {
                return !e.getAttribute("class").matches(".*\\b" + cssClass + "\\b.*");
            }
        });
    }

    public static void waitForStyleAbsent(final WebElement e, final String style) {
        WebDriverWait wait = new WebDriverWait(getCurrentDriver(), Constants.TIMEOUT);
        wait.until(new Function<Object, Boolean>() {
            @Override
            public Boolean apply(Object arg0) {
                return !e.getAttribute("style").matches(".*\\b" + style + "\\b.*");
            }
        });
    }

}

Log Out

The Home container

public class HomeContainer {

    @FindBy(how = How.CSS, using = "#ECMWebUI div.ecmBanner")
    public WebElement mainBanner;

    private static final String visibleTab = "//div[@id='ECMWebUI']/div[contains(@class, 'ecmLayout')]/div/div[contains(@class, 'dijitStackContainer')]/div[contains(@class, 'dijitVisible') and ./div[@data-dojo-attach-point='mainPane']]";

    @FindBy(how = How.XPATH, using = visibleTab)
    public WebElement desktopTab;

    @FindBy(how = How.XPATH, using = "//div[contains(@class, 'ecmBanner')]//span[@title='User session']")
    public WebElement userMenuDropDown;

    private static final String logOutMenuItemXpath = "//div[contains(@class, 'dijitMenuPopup') and not(contains(@style, 'display: none'))]//td[contains(@class, 'dijitMenuItemLabel') and text()='Log Out']";

    @FindBy(how = How.XPATH, using = logOutMenuItemXpath)
    public WebElement logOutMenuItem;

    @FindBy(how = How.XPATH, using = logOutMenuItemXpath + "/span[contains(@class, 'dijitButtonText')]")
    public WebElement userMenuDropDownLabel;

}

And the HomeView

public class HomeView {
	private static final HomeContainer homeContainer = PageFactory.initElements(BrowserDriver.getCurrentDriver(), HomeContainer.class);

	public static void isDisplayedCheck() throws Exception{
		System.out.println("Checking login page is displayed");
		BrowserDriver.waitEcmWait();
		BrowserDriver.waitForElement(homeContainer.desktopTab);
	}

	public static void logOut(){
		System.out.println("Logging Out...");
		BrowserDriver.waitEcmWait();
		homeContainer.userMenuDropDown.click();
		BrowserDriver.waitForElement(homeContainer.logOutMenuItem);
		homeContainer.logOutMenuItem.click();
		DialogView.checkConfirmationDialogDisplayed();
		DialogView.acceptConfirmDialog("Log Out");
		System.out.println("LogOut submitted");
	}

}

And for this one we need a generic view managing common dialogs, comming right now:

Dealing with Message and Confirmation dialog

Container

public class DialogContainer {

    @FindBy(how = How.XPATH, using = "//div[contains(@class, 'ecmConfirmationDialog') and not(contains(@style, 'display: none'))]")
    public WebElement confirmationDialog;

    @FindBy(how = How.XPATH, using = "//div[contains(@class, 'ecmMessageDialog') and not(contains(@style, 'display: none'))]")
    public WebElement messageDialog;

    public static final String buttonByNameXpath = ".//div[contains(@class, 'ecmDialogPaneActionBar')]//span[contains(@class, 'dijitButtonText') and text()='%s']";

    public static final String messageArea = ".//div[contains(@class, 'messageNode')]//span[contains(@class, 'description')]";

}

View

public class DialogView {
    private static final DialogContainer dialogContainer = PageFactory.initElements(BrowserDriver.getCurrentDriver(), DialogContainer.class);

    public static void checkConfirmationDialogDisplayed() {
        BrowserDriver.waitForElement(dialogContainer.confirmationDialog);
    }

    public static void checkMessageDialogDisplayed() {
        BrowserDriver.waitForElement(dialogContainer.messageDialog);
    }

    public static void acceptConfirmDialog(String buttonLabel) {
        System.out.println("Accepting confirmationDialog with button " + buttonLabel);
        WebElement label = dialogContainer.confirmationDialog.findElement(By.xpath(String.format(DialogContainer.buttonByNameXpath, buttonLabel)));
        label.click();
        BrowserDriver.waitEcmWait();
    }

    public static void closeMessageDialog() {
        System.out.println("Closing Message Dialog");
        WebElement label = dialogContainer.messageDialog.findElement(By.xpath(String.format(DialogContainer.buttonByNameXpath, "Close")));
        label.click();
        BrowserDriver.waitEcmWait();
    }

    public static void cancelConfirmationDialog() {
        System.out.println("Cancelling Confirmation Dialog");
        WebElement label = dialogContainer.confirmationDialog.findElement(By.xpath(String.format(DialogContainer.buttonByNameXpath, "Cancel")));
        label.click();
        BrowserDriver.waitEcmWait();
    }

    public static boolean doesMessageDialogContainsText(String message) {
        WebElement messageArea = dialogContainer.messageDialog.findElement(By.xpath(DialogContainer.messageArea));
        return messageArea.getText().contains(message);
    }

    public static boolean doesConfirmationDialogContainsText(String text) {
        WebElement messageArea = dialogContainer.confirmationDialog.findElement(By.xpath(DialogContainer.messageArea));
        return messageArea.getText().contains(text);
    }

}

Add a folder

Let’s see how to call the New Folder Wizard, enter a name and validate

Container:

public class AddFolderWizardContainer {

    private static final String dialogXpath = "//div[contains(@class, 'ecmBaseDialog') and contains(@id, 'ecm_widget_dialog_AddContentItemDialog_') and not(contains(@style, 'display: none'))]";

    @FindBy(how = How.XPATH, using = dialogXpath + "//div[contains(@class, 'commonPropertiesDiv')]//input[@name='FolderName']")
    public WebElement folderNameInput;

    @FindBy(how = How.XPATH, using = dialogXpath
            + "//div[contains(@class,'ecmDialogPaneActionBar')]//span[contains(@class, 'dijitButton') and .//span[contains(@class, 'dijitButtonText') and text()='Add']]")
    public WebElement addButton;

    @FindBy(how = How.XPATH, using = dialogXpath
            + "//div[contains(@class,'ecmDialogPaneActionBar')]//span[contains(@class, 'dijitButtonText') and text()='Add']")
    public WebElement addButtonLabel;

    @FindBy(how = How.XPATH, using = dialogXpath)
    public WebElement dialog;

    public static final String buttonDisabledClass = "dijitButtonDisabled";

}

View

public class AddFolderWizardView {
    private static final AddFolderWizardContainer container = PageFactory.initElements(BrowserDriver.getCurrentDriver(),
            AddFolderWizardContainer.class);

    public static void enterFolderName(String name) {
        BrowserDriver.waitForElement(container.folderNameInput);
        container.folderNameInput.sendKeys(name);
    }

    public static void valid() {
        BrowserDriver.waitEcmWait();
        BrowserDriver.waitForCssClassNotPresent(container.addButton, AddFolderWizardContainer.buttonDisabledClass);
        BrowserDriver.waitForElement(container.addButtonLabel);
        BrowserDriver.waitClickable(container.addButtonLabel);
        container.addButtonLabel.click();
    }

    public static void waitToBeReady() {
        BrowserDriver.waitForStyleAbsent(container.dialog, "display: none");
        BrowserDriver.waitEcmWait();
    }

    public static void createFolder(String name) {
        System.out.println("Writing name");
        enterFolderName(name);
        System.out.println("Validating");
        valid();
    }

}

And you will need a way to launch the New Folder Wizard, for that I’ve written a container for the BrowsePane:

Browse the Browse Pane

Container

public class BrowseContainer {

    public static final By nameColumnIdx = By.xpath(".//div[contains(@class, 'gridxHeader')]//td[./div[text()='Name']]");

    private static final String nameColumnValueXpath = ".//td[@colid='%s']";

    public static final By rowsSelector = By.cssSelector("div.gridxRow");

    private static final String rowNameLabelXpath = ".//td[text() = '%s']";

    public static final By getNameColumnValueXpath(int nameColumnIdx) {
        return By.xpath(String.format(nameColumnValueXpath, nameColumnIdx));
    }

    public static final By getRowlabel(String name) {
        return By.xpath(String.format(rowNameLabelXpath, name));
    }

    @FindBy(how = How.CSS, using = "div.dijitStackContainerChildWrapper[aria-label='Browse'] div.dijitTreeIsRoot > div.dijitTreeNodeContainer")
    public WebElement treeContainer;

    @FindBy(how = How.CSS, using = "div.dijitStackContainerChildWrapper[aria-label='Browse'] div.dijitTreeIsRoot > div.dijitTreeRow > span.dijitTreeContent > span.dijitTreeLabel")
    public WebElement repositoryName;

    @FindBy(how = How.CSS, using = "div.dijitStackContainerChildWrapper[aria-label='Browse'] div.ecmContentList")
    public WebElement contentList;

    @FindBy(how = How.CSS, using = "div.dijitStackContainerChildWrapper[aria-label='Browse'] div.dijitContentPane.topContainer table.Bar div.breadcrumb")
    public WebElement breadcrumb;

    @FindBy(how = How.CSS, using = "div.dijitStackContainerChildWrapper[aria-label='Browse'] div.dijitTreeIsRoot")
    public WebElement tree;

    @FindBy(how = How.XPATH, using = "//div[contains(@class, 'dijitStackContainerChildWrapper') and @aria-label='Browse']//div[contains(@class, 'ecmToolbar')]//span[contains(@class, 'dijitButtonText') and text()='Refresh']")
    public WebElement refreshLink;

    @FindBy(how = How.XPATH, using = "//div[contains(@class, 'dijitStackContainerChildWrapper') and @aria-label='Browse']//div[contains(@class, 'ecmToolbar')]//span[contains(@class, 'dijitButtonText') and text()='New Folder']")
    public WebElement newFolderLink;

    @FindBy(how = How.XPATH, using = "//div[contains(@class, 'dijitStackContainerChildWrapper') and @aria-label='Browse']//div[contains(@class, 'ecmToolbar')]//span[contains(@class, 'dijitButtonText') and text()='Add Document']")
    public WebElement newDocumentLink;

    @FindBy(how = How.XPATH, using = "//div[contains(@class, 'dijitStackContainerChildWrapper') and @aria-label='Browse']//div[contains(@class, 'ecmToolbar')]//span[contains(@class, 'dijitButtonText') and text()='Actions']")
    public WebElement actionsLink;

}

View

public class BrowseView {
    private static final BrowseContainer browseContainer = PageFactory.initElements(BrowserDriver.getCurrentDriver(), BrowseContainer.class);

    private static TreeView treeView = null;

    public static void createDocumentInCurrentLocation(String name) {
        browseContainer.newDocumentLink.click();
        AddDocumentWizardView.waitToBeReady();
        System.out.println("AddDocument ready");
        AddDocumentWizardView.createDocument(name);
        waitForItemInContentPane(name);
    }

    public static void createFolderInCurrentLocation(String name) {
        browseContainer.newFolderLink.click();
        AddFolderWizardView.waitToBeReady();
        System.out.println("AddFolder ready");
        AddFolderWizardView.createFolder(name);
        waitForItemInContentPane(name);
    }

    public static WebElement getTreeRoot() {
        return browseContainer.tree;
    }

    public static TreeView getTreeView() {
        if (treeView == null) {
            treeView = TreeView.createTreeView(getTreeRoot());
        }
        return treeView;
    }

    public static boolean openPath(String path, boolean createIfNeeded) {
        return getTreeView().moveToPath(path, createIfNeeded, true);
    }

    public static void deleteFolder(String path) {
        if (getTreeView().moveToPath(path, false, true)) {
            getTreeView().openNodeMenu();
            ContextMenuView.clickMenu("Admin Delete");
        } else {
            Assert.fail(path + " does not exist");
        }
    }

    public static boolean checkFolderExist(String folderPath) {
        return getTreeView().pathExist(folderPath);
    }

    public static void waitForItemInContentPane(final String name) {
        WebDriverWait wait = new WebDriverWait(BrowserDriver.getCurrentDriver(), Constants.TIMEOUT);
        wait.until(new Function<Object, Boolean>() {
            @Override
            public Boolean apply(Object arg0) {
                return !browseContainer.contentList.findElements(BrowseContainer.getRowlabel(name)).isEmpty();
            }
        });
    }

    public static boolean isContentDisplayed(String folderPath) {
        // Remove first and possible last /
        BrowserDriver.waitEcmWait();
        if (folderPath.startsWith("/")) {
            folderPath = folderPath.substring(1);
        }
        if (folderPath.endsWith("/")) {
            folderPath = folderPath.substring(0, folderPath.length() - 1);
        }
        String[] paths = folderPath.split("/");
        List divs = browseContainer.breadcrumb.findElements(By
                .xpath(".//a/div|.//span/div[not(contains(@class, 'breadcrumbItemSeparator'))]"));
        // Remove the first one if it's the object store
        if (divs.get(0).getText().equals(browseContainer.repositoryName.getText())) {
            System.out.println("Removing repository name " + browseContainer.repositoryName.getText());
            divs.remove(0);
        }
        System.out.print("Comparing " + folderPath + " and ");
        for (WebElement div : divs) {
            System.out.print(div.getText() + ",");
        }
        System.out.println();
        if (paths.length != divs.size()) {
            return false;
        }
        for (int i = 0; i < paths.length - 1; i++) {
            String p = paths[i];
            if (!p.equalsIgnoreCase(divs.get(i).getText())) {
                return false;
            }
        }
        return true;
    }

    public static List getAllDocumentnamesInCurrentFolder() {
        return getAllItemInCurrentFolder(false, true);
    }

    public static List getAllFolderNamesInCurrentFolder() {
        return getAllItemInCurrentFolder(true, false);
    }

    public static List getAllNamesInCurrentFolder() {
        return getAllItemInCurrentFolder(true, true);
    }

    private static List getAllItemInCurrentFolder(boolean includeFolder, boolean includeDoc) {
        List res = new ArrayList();
        // Count the location of the column Name

        int nameIdx = findNameColumnIdx();

        // Refresh the folder to make sure we have an up-to-date view of the repository
        refreshCurrentFolder();

        // Now we get all tr in the content
        List rows = browseContainer.contentList.findElements(BrowseContainer.rowsSelector);
        // for each row we check if it's a document
        for (WebElement row : rows) {
            boolean isFolder = row.getAttribute("rowId").contains("Folder");
            if ((!isFolder && includeDoc) || (isFolder && includeFolder)) {
                WebElement nameTag = row.findElement(BrowseContainer.getNameColumnValueXpath(nameIdx));
                res.add(nameTag.getText());
            }
        }
        return res;
    }

    private static int findNameColumnIdx() {
        WebElement td = browseContainer.contentList.findElement(BrowseContainer.nameColumnIdx);
        String colIdS = td.getAttribute("colid");
        int nameIdx = -1;
        if (colIdS != null && !colIdS.equals("")) {
            try {
                nameIdx = Integer.parseInt(colIdS);
            } catch (NumberFormatException e) {
            }
        }
        if (nameIdx == -1) {
            System.out.println("Can not found column id for column 'Name'");
            throw new RuntimeException("Can not found column id for column 'Name'");
        }
        return nameIdx;
    }

    public static boolean selectAllDocumentsInCurrentFolder() {
        return selectAllItemInCurrentFolder(false, true);
    }

    public static boolean selectAllFoldersInCurrentFolder() {
        return selectAllItemInCurrentFolder(true, false);
    }

    public static boolean selectAllInCurrentFolder() {
        return selectAllItemInCurrentFolder(true, true);
    }

    private static boolean selectAllItemInCurrentFolder(boolean includeFolder, boolean includeDoc) {
        boolean result = false;
        // Start pressing CTRL
        Actions actions = new Actions(BrowserDriver.getCurrentDriver());
        actions = actions.keyDown(Keys.CONTROL);

        // Now we get all tr in the content
        List rows = browseContainer.contentList.findElements(By.cssSelector("div.gridxRow"));
        // for each row we check if it's a document
        for (WebElement row : rows) {
            boolean isFolder = row.getAttribute("rowId").contains("Folder");
            if ((!isFolder && includeDoc) || (isFolder && includeFolder)) {
                // click on the table, not he row
                actions = actions.click(row.findElement(By.cssSelector("table")));
                result = true;
            }
        }

        // We can stop pressing CTRL now
        actions = actions.keyUp(Keys.CONTROL);
        // And launch the actions
        actions.build().perform();
        return result;
    }

    public static boolean selectItemByNameInCurrentFolder(String name, boolean includeFolder, boolean includeDoc) {
        boolean result = false;
        // Start pressing CTRL
        Actions actions = new Actions(BrowserDriver.getCurrentDriver());
        actions = actions.keyDown(Keys.CONTROL);

        int nameIdx = findNameColumnIdx();

        // Now we get all tr in the content
        List rows = browseContainer.contentList.findElements(By.xpath("//div[contains(@class, 'gridxRow') and .//td[@colid='" + nameIdx
                + "' and text()='" + name + "']]"));
        // for each row we check if it's a document
        for (WebElement row : rows) {
            boolean isFolder = row.getAttribute("rowId").contains("Folder");
            if ((!isFolder && includeDoc) || (isFolder && includeFolder)) {
                // click on the table, not he row
                actions = actions.click(row.findElement(By.cssSelector("table")));
                result = true;
            }
        }

        // We can stop pressing CTRL now
        actions = actions.keyUp(Keys.CONTROL);
        // And launch the actions
        actions.build().perform();
        return result;
    }

    public static void deleteRecursively(String folderPath) {
        if (!getTreeView().moveToPath(folderPath, false, true)) {
            Assert.fail(folderPath + " does not exist");
        } else {
            deleteRecursivelyCurrent();

            // Then finally delete the folder
            getTreeView().openNodeMenu();
            ContextMenuView.clickMenu("Admin Delete");
            DialogView.checkConfirmationDialogDisplayed();
            DialogView.acceptConfirmDialog("Delete");
        }
    }

    private static void deleteRecursivelyCurrent() {
        BrowserDriver.waitEcmWait();

        // Expand the current node if it is node already done
        if (getTreeView().isNodeClosed()) {
            getTreeView().expandNode();
        }

        //  Empty all folder subfolder
        List subfolders = getAllFolderNamesInCurrentFolder();
        for (String subfolder : subfolders) {
            getTreeView().moveToChild(subfolder, true);
            deleteRecursivelyCurrent();
            getTreeView().moveToParent(true);
        }

        BrowserDriver.waitEcmWait();

        // And delete all folder and documents
        if (selectAllInCurrentFolder()) {
            deleteSelectedItems();
        }

    }

    private static void deleteSelectedItems() {
        browseContainer.actionsLink.click();
        ContextMenuView.waitContextMenuisReady();
        ContextMenuView.clickMenu("Admin Delete");
        DialogView.checkConfirmationDialogDisplayed();
        DialogView.acceptConfirmDialog("Delete");
    }

    public static void refreshCurrentFolder() {
        browseContainer.refreshLink.click();
        BrowserDriver.waitEcmWait();
    }

    public static void openActionMenu() {
        browseContainer.actionsLink.click();
        ContextMenuView.waitContextMenuisReady();
    }

    public static void closeActionMenu() {
        browseContainer.actionsLink.click();
    }

    public static void fileIn(String docName, String targetFolder) {
        if (selectItemByNameInCurrentFolder(docName, false, true)) {
            browseContainer.actionsLink.click();
            ContextMenuView.waitContextMenuisReady();
            ContextMenuView.clickMenu("Folders>Add to Folder");
            FolderOperationsView.waitForDialogReady();
            FolderOperationsView.selectPath(targetFolder);
        } else {
            Assert.fail("No document named " + docName + " in current folder.");
        }

    }

    public static void clickNewFolderButton() {
        browseContainer.newFolderLink.click();
    }

    public static void clickAddDocumentButton() {
        browseContainer.newDocumentLink.click();
    }

}

And finally to make this work, we need to manage Context Menus and also navigate withing the tree.

Navigate within a dojo Tree

Container

public class TreeContainer {

    public static final By expando = By.cssSelector("span.dijitTreeExpando");
    public static final String leafClass = "dijitTreeExpandoLeaf";
    public static final String loadingClass = "dijitTreeExpandoLoading";
    public static final String closedClass = "dijitTreeExpandoClosed";
    public static final String notSelectableClass = "ecmFolderNotSelectable";
    private static final String xPath_childLabel = "./div[contains(@class, 'dijitTreeNodeContainer')]/div[contains(@class, 'dijitTreeNode')]/div/span/span[contains(@class,'dijitTreeLabel') and text() = '%s']";
    public static final By xPath_NodeRow = By.xpath("./div[contains(@class, 'dijitTreeRow')]");

    public static final By getXPathChildLabel(String childName) {
        return By.xpath(String.format(xPath_childLabel, childName));
    }

    public static final WebElement getNodeFromLabel(WebElement label) {
        return label.findElement(By.xpath("..")).findElement(By.xpath("..")).findElement(By.xpath(".."));
    }

    public static final WebElement getParentNode(WebElement node) {
        return node.findElement(By.xpath("..")).findElement(By.xpath(".."));
    }

}

View

public class TreeView {

    private WebElement root;
    private WebElement currentNode;
    private LinkedList currentPath = new LinkedList();

    private TreeView(WebElement root) {
        this.root = root;
        currentNode = root;
    }

    /**
     * Create a new tree with the root given as argument
     *
     * @param root the root
     * @return a new {@link TreeView}
     */
    public static TreeView createTreeView(WebElement root) {
        return new TreeView(root);
    }

    /**
     * @return <code>true</code> if the current node is a leaf, <code>false</code> otherwise
     */
    public boolean isNodeLeaf() {
        WebElement expando = currentNode.findElement(TreeContainer.expando);
        return expando.getAttribute("class").contains(TreeContainer.leafClass);
    }

    /**
     * Wait that the current node finishes loading
     */
    public void waitForNodeReady() {
        WebElement expando = currentNode.findElement(TreeContainer.expando);
        BrowserDriver.waitForCssClassNotPresent(expando, TreeContainer.loadingClass);
    }

    /**
     * @return <code>true</code> if the current node is closed (i.e. not expanded), <code>false</code> otherwise
     */
    public boolean isNodeClosed() {
        WebElement expando = currentNode.findElement(TreeContainer.expando);
        return expando.getAttribute("class").contains(TreeContainer.closedClass);
    }

    /**
     * Expand the current node and then return true id it's a leaf, false if it has sub-folders
     *
     * @return <code>true</code> if it's a leaf, <code>false</code> otherwise
     */
    public boolean expandNode() {
        WebElement expando = currentNode.findElement(TreeContainer.expando);
        expando.click();
        waitForNodeReady();
        return expando.getAttribute("class").contains(TreeContainer.leafClass);
    }

    /**
     * Wait for the root node to be loaded
     */
    public void waitForTreeLoaded() {

        WebDriverWait wait = new WebDriverWait(BrowserDriver.getCurrentDriver(), Constants.TIMEOUT);
        wait.until(new Function<Object, Boolean>() {
            @Override
            public Boolean apply(Object arg0) {
                return !root.getText().trim().isEmpty();
            }
        });
    }

    /**
     * Check if the current node has a child named <code>name</code>
     *
     * @param name name of the child (case sensitive)
     * @return <code>true</code> if the current node has a child named <code>name</code>, <code>false</code> if not
     */
    public boolean hasChild(String name) {
        System.out.println("Does child " + name + " exist");
        boolean res = false;
        try {
            currentNode.findElement(TreeContainer.getXPathChildLabel(name));
            res = true;
        } catch (NoSuchElementException e) {
        }
        return res;
    }

    /**
     * Move the current node to the current node's child named <code>name</code>. Does not open the child node.
     *
     * @param name child's name
     * @return <code>true</code> if moved successfully, <code>false</code> if there is no child named <code>name</code>
     */
    public boolean moveToChild(String name) {
        return moveToChild(name, false);
    }

    /**
     * Move the current node to the current node's child named <code>name</code>. Open the child node of
     * <code>open</code> is <code>true</code>.
     *
     * @param name child's name
     * @param open if <code>true</code>, the child node is opened (click on it)
     * @return <code>true</code> if moved successfully, <code>false</code> if there is no child named <code>name</code>
     */
    public boolean moveToChild(String name, boolean open) {
        System.out.println("Move to child " + name);
        boolean res = false;
        try {
            WebElement label = currentNode.findElement(TreeContainer.getXPathChildLabel(name));
            currentNode = TreeContainer.getNodeFromLabel(label);
            currentPath.add(name);
            res = true;
            if (open) {
                openNode();
            }
        } catch (NoSuchElementException e) {
        }
        return res;
    }

    /**
     * Move the current node to the current node's parent. Does not open the parent node.
     *
     * @return <code>true</code> if moved successfully, <code>false</code> if the current node has no parent.
     */
    public boolean moveToParent() {
        return moveToParent(false);
    }

    /**
     * Move the current node to the current node's parent. Does not open the parent node.
     *
     * @param open if <code>true</code>, the parent node is opened (click on it)
     * @return <code>true</code> if moved successfully, <code>false</code> if the current node has no parent.
     */
    public boolean moveToParent(boolean open) {
        System.out.println("Move to parent ");
        boolean res = false;
        try {
            currentNode = TreeContainer.getParentNode(currentNode);
            currentPath.pop();
            res = true;
            if (open) {
                openNode();
            }
        } catch (NoSuchElementException e) {
        }
        return res;
    }

    /**
     *
     * @return the current node
     */
    public WebElement getCurrentNode() {
        return currentNode;
    }

    /**
     * Browse the tree from the root to the current node again if for any reason the current node is not up-to-date
     */
    public void refreshCurrentNode() {
        StringBuffer buf = new StringBuffer();
        for (String s : currentPath) {
            buf.append("/");
            buf.append(s);
        }
        moveToPath(buf.toString(), false, true);
    }

    /**
     * Move back to the root as current node
     */
    public void moveOnRoot() {
        currentNode = root;
        currentPath.clear();
        openNode();
    }

    /**
     * Open the current node (Click on it)
     */
    public void openNode() {
        clickLabel();
    }

    /**
     * Move the current node to the given path. If the path starts with /, this is an absolute path from the root, if it
     * doesn't, this is a relative path from the current node
     *
     * @param path the relative or absolute path
     * @param createIfNeeded <code>true</code> is we want to create missing folder, if <code>false</code> the method
     *            will return <code>false</code> if there is missing folder on the path
     * @param refreshAll if <code>true</code>, refresh all node via the content panel. Only when navigating in the
     *            repository tree, do not use in other tree.
     * @return <code>true</code> if the path exist and <code>createIfNeeded</code> if <code>false</code>, always
     *         <code>true</code> if <code>createIfNeeded</code> is <code>true</code> since folder are created if needed
     */
    public boolean moveToPath(String path, boolean createIfNeeded, boolean refreshAll) {
        // Look for the node name
        System.out.println("Expending " + path);

        // If path start with a /, this is a absolute path, start from the root
        if (path.startsWith("/")) {
            moveOnRoot();
            path = path.substring(1);
        }

        // If path is now empty, we are on the right node
        if (path.isEmpty()) {
            clickLabel();
            if (refreshAll) {
                BrowseView.refreshCurrentFolder();
            }
            return true;
        }

        // Find the child's name
        String folderName = path.indexOf("/") != -1 ? path.substring(0, path.indexOf("/")) : path;
        // Prepare the left path
        String leftPath = path.substring(folderName.length());
        if (leftPath.startsWith("/")) {
            leftPath = leftPath.substring(1);
        }

        // Parent case
        if ("..".equals(folderName)) {
            if (!moveToParent()) {
                return false;
            }
            return moveToPath(leftPath, createIfNeeded, refreshAll);
        }

        // For children case we refresh to be sure we have an up-to-date folder

        // Is the node is closed, expand it to look for the child
        if (isNodeClosed()) {
            // We need to expand the node for next step
            expandNode();
        }

        // Open current node and refresh to make sure the children isn't a ghost node
        clickLabel();
        if (refreshAll) {
            BrowseView.refreshCurrentFolder();
        }

        if (hasChild(folderName)) {
            // Continue recursive process
            moveToChild(folderName);
            return moveToPath(leftPath, createIfNeeded, refreshAll);

        } else if (createIfNeeded) {
            // The child does not exist but we want to create it
            BrowseView.createFolderInCurrentLocation(folderName);
            BrowseView.refreshCurrentFolder();
            waitForNodeReady();

            // Move on the new child and continue
            moveToChild(folderName);
            return moveToPath(leftPath, createIfNeeded, refreshAll);

        } else {
            // return false because the path does not exist and we don't want to create it
            return false;
        }

    }

    /**
     * Check if a path exist. The path is an absolute path (from the root) if it starts with a /, and an absolute path
     * (from the current node), if it doesn't start with a /
     *
     * @param path the absolute or relative path
     * @return <code>true</code> if the path exist, <code>false</code> otherwise
     */
    public boolean pathExist(String path) {
        return moveToPath(path, false, true);
    }

    /**
     * Click on the label even if the node is expanded (click on the node will click in the middle and select a child)
     *
     */
    private void clickLabel() {
        WebElement label = currentNode.findElement(TreeContainer.xPath_NodeRow);
        label.click();
        BrowserDriver.waitEcmWait();
        waitForNodeReady();
    }

    /**
     * Open the context menu of the current node
     */
    public void openNodeMenu() {
        WebElement label = currentNode.findElement(TreeContainer.xPath_NodeRow);
        Actions actions = new Actions(BrowserDriver.getCurrentDriver());
        actions.contextClick(label).build().perform();
        ContextMenuView.waitContextMenuisReady();
    }

    public boolean isNodeDisabled() {
        WebElement nodeTitle = currentNode.findElement(TreeContainer.xPath_NodeRow);
        return nodeTitle.getAttribute("class").contains(TreeContainer.notSelectableClass);
    }

}

Deal with Context Menu

Container

public class ContextMenuContainer {

    @FindBy(how = How.CSS, using = "div.dijitMenuPopup table.dijitMenuActive")
    public WebElement popupMenu;

    public WebElement menuItemByName(String name) {
        return popupMenu.findElement(By.xpath(".//tr[contains(@class, 'dijitMenuItem')]/td[contains(@class, 'dijitMenuItemLabel') and text()='"
                + name + "']"));
    }

    public WebElement subMenuItemByName(WebElement menuItem, String name) {
        String labelId = menuItem.getAttribute("id");
        WebElement subMenu = BrowserDriver.getCurrentDriver().findElement(
                By.xpath("//table[contains(@class, 'dijitMenuTable') and @aria-labelledby='" + labelId + "']"));
        return subMenu.findElement(By.xpath(".//tr[contains(@class, 'dijitMenuItem')]/td[contains(@class, 'dijitMenuItemLabel') and text()='" + name
                + "']"));
    }

    public void clckMenuItem(String name) {
        WebElement menuItem = menuItemByName(name);
        menuItem.click();
    }

    public void clickSubMenuItem(WebElement menuItem, String name) {
        String labelId = menuItem.getAttribute("id");
        WebElement subMenu = BrowserDriver.getCurrentDriver().findElement(
                By.xpath("//table[contains(@class, 'dijitMenuTable') and @aria-labelledby='" + labelId + "']"));
        WebElement labelToClick = subMenu.findElement(By
                .xpath(".//tr[contains(@class, 'dijitMenuItem')]/td[contains(@class, 'dijitMenuItemLabel') and text()='" + name + "']"));
        labelToClick.click();
    }

}

View

public class ContextMenuView {
    private static final ContextMenuContainer contextMenuContainer = PageFactory.initElements(BrowserDriver.getCurrentDriver(),
            ContextMenuContainer.class);

    public static void waitContextMenuisReady() {
        System.out.println("Waiting for popup menu");
        WebDriverWait wait = new WebDriverWait(BrowserDriver.getCurrentDriver(), Constants.TIMEOUT);
        wait.until(ExpectedConditions.visibilityOf(contextMenuContainer.popupMenu));
    }

    public static void clickMenu(String name) {
        System.out.println("Clicking " + name);
        if (name.contains(">")) {
            String[] items = name.split(">");
            contextMenuContainer.clckMenuItem(items[0]);
            WebElement item = contextMenuContainer.menuItemByName(items[0]);
            contextMenuContainer.clickSubMenuItem(item, items[1]);
        } else {
            contextMenuContainer.clckMenuItem(name);
        }
    }

    public static boolean doesMenuItemExist(String name) {
        boolean res = false;
        if (name.contains(">")) {
            String[] items = name.split(">");
            try {
                WebElement item = contextMenuContainer.menuItemByName(items[0]);
                contextMenuContainer.subMenuItemByName(item, items[1]);
                res = true;
            } catch (NoSuchElementException e) {
            }
        } else {
            try {
                contextMenuContainer.menuItemByName(name);
                res = true;
            } catch (NoSuchElementException e) {
            }
        }
        return res;
    }

}

Add a document

Container

public class AddFolderWizardContainer {

    private static final String dialogXpath = "//div[contains(@class, 'ecmBaseDialog') and contains(@id, 'ecm_widget_dialog_AddContentItemDialog_') and not(contains(@style, 'display: none'))]";

    @FindBy(how = How.XPATH, using = dialogXpath + "//div[contains(@class, 'commonPropertiesDiv')]//input[@name='FolderName']")
    public WebElement folderNameInput;

    @FindBy(how = How.XPATH, using = dialogXpath
            + "//div[contains(@class,'ecmDialogPaneActionBar')]//span[contains(@class, 'dijitButton') and .//span[contains(@class, 'dijitButtonText') and text()='Add']]")
    public WebElement addButton;

    @FindBy(how = How.XPATH, using = dialogXpath
            + "//div[contains(@class,'ecmDialogPaneActionBar')]//span[contains(@class, 'dijitButtonText') and text()='Add']")
    public WebElement addButtonLabel;

    @FindBy(how = How.XPATH, using = dialogXpath)
    public WebElement dialog;

    public static final String buttonDisabledClass = "dijitButtonDisabled";

}

View

public class AddDocumentWizardView {
    private static final AddDocumentWizardContainer container = PageFactory.initElements(BrowserDriver.getCurrentDriver(),
            AddDocumentWizardContainer.class);

    public static void enterDocumentName(String name) {
        BrowserDriver.waitForElement(container.documentTitleInput);
        container.documentTitleInput.sendKeys(name);
    }

    public static void selectEntryTemplate(String name) {
        BrowserDriver.waitForElement(container.ETNameInput);
        container.ETNameInput.sendKeys(name);
        container.ETNameInput.sendKeys(Keys.RETURN);
        BrowserDriver.waitEcmWait();
    }

    public static void selectContentSourceType(String name) {
        container.contentSourceButton.click();
        waitPopupToBeReady();
        clickPopupMenuItem(name);
    }

    private static void clickPopupMenuItem(String name) {
        WebElement menuItem = container.contentSourceTypePopupMenu.findElement(By.xpath(".//td[contains(@class, 'ijitMenuItemLabel') and text()='"
                + name + "']"));
        menuItem.click();
    }

    public static void valid() {
        BrowserDriver.waitClickable(container.addButton);
        container.addButton.click();
    }

    public static void waitToBeReady() {
        BrowserDriver.waitForStyleAbsent(container.dialog, "display: none");
        BrowserDriver.waitEcmWait();
    }

    public static void waitPopupToBeReady() {
        BrowserDriver.waitForStyleAbsent(container.contentSourceTypePopupMenu, "display: none");
        BrowserDriver.waitEcmWait();
    }

    public static void createDocument(String name) {
        System.out.println("Selecting template");
        selectEntryTemplate("my entry template");
        BrowserDriver.waitEcmWait();
        System.out.println("Selecting source");
        selectContentSourceType("Information about a document");
        BrowserDriver.waitEcmWait();
        System.out.println("Writing name");
        enterDocumentName(name);
        System.out.println("Validating");
        valid();
        BrowserDriver.waitEcmWait();
    }

}

File document and move them

Container

public class FolderOperationsContainer {

    private static final String moveFileDialogXpath = "//div[contains(@class, 'ecmMoveFileDialog') and not(contains(@style, 'display: none'))]";

    @FindBy(how = How.XPATH, using = moveFileDialogXpath
            + "//div[contains(@class, 'ecmDialogPaneActionBar')]//span[contains(@class, 'dijitButtonText') and text()='Add']")
    public WebElement addButton;

    @FindBy(how = How.XPATH, using = moveFileDialogXpath
            + "//div[contains(@class, 'ecmDialogPaneActionBar')]//span[contains(@class, 'dijitButtonText') and text()='Cancel']")
    public WebElement cancelButton;

    @FindBy(how = How.XPATH, using = moveFileDialogXpath
            + "//div[contains(@class, 'ecmFolderSelector')]/div[contains(@class, 'ecmFolderTree')]/div[@role='tree']/div/div[contains(@class, 'dijitTreeIsRoot')]")
    public WebElement treeRoot;

}

View

public class FolderOperationsView {
    private static final FolderOperationsContainer folderContainer = PageFactory.initElements(BrowserDriver.getCurrentDriver(),
            FolderOperationsContainer.class);

    public static final void waitForDialogReady() {
        BrowserDriver.waitForElement(folderContainer.treeRoot);
    }

    public static final void selectPath(String path) {
        TreeView treeView = TreeView.createTreeView(folderContainer.treeRoot);
        treeView.moveToPath(path, false, false);
        folderContainer.addButton.click();
    }

    public static final void cancelDialog() {
        folderContainer.cancelButton.click();
    }

    public static boolean isNodeDisable(String path) {
        TreeView treeView = TreeView.createTreeView(folderContainer.treeRoot);
        treeView.moveToPath(path, false, false);
        return treeView.isNodeDisabled();
    }
}

 

4 thoughts on “Test your ICN plug-in’s UI with Selenium

  1. Kim

    Hi, we are looking into automating the UI testing for ICN as well and stumbled across your post, this is very helpful!

    Any chance I could download the source code as well?

    Reply
    1. Guillaume Post author

      Hi Kim,

      I apologize for the really late answer, but I changed my hosting in January and I just noticed emails weren’t working anymore so I missed all the comments.

      Not really because it’s part of an internal project not open source. I’ll try to see if I can release some pieces as open source soon, but meanwhile feel free to ask questions if you need help.

      Reply
  2. Mark E Zawadzki

    “public static void waitDojoReady() ” fails with

    org.openqa.selenium.JavascriptException: ReferenceError: Test_1502818291920 is not defined
    Build info: version: ‘3.4.0’, revision: ‘unknown’, time: ‘unknown’
    System info: host: ‘MZAWADZKI-LT’, ip: ‘192.168.0.110’, os.name: ‘Windows 10’, os.arch: ‘amd64’, os.version: ‘10.0’, java.version: ‘1.8.0_131’
    Driver info: org.openqa.selenium.firefox.FirefoxDriver
    Capabilities [{moz:profile=C:\Users\MZAWAD~1\AppData\Local\Temp\rust_mozprofile.PQaxkAulnACs, rotatable=false, timeouts={implicit=0.0, pageLoad=300000.0, script=30000.0}, pageLoadStrategy=normal, platform=ANY, specificationLevel=0.0, moz:accessibilityChecks=false, acceptInsecureCerts=true, browserVersion=54.0.1, platformVersion=10.0, moz:processID=14860.0, browserName=firefox, javascriptEnabled=true, platformName=windows_nt}]

    Reply

Leave a Reply