Selenium is a popular tool for user interface testing, and it can be useful for teams that are rolling out Nuxeo. Essentially, Selenium operates Firefox or Chrome to interact with a website through the browser (rather than through API calls) based on scripts and reports on the results. You can use it, for example, to automate some of your regression testing to make sure that certain base functionality has not been inadvertently impacted by new features and that the right user roles still have the right permissions. However, you should be aware of Nuxeo’s use of the Shadow DOM when using Selenium.
Finding elements in the Nuxeo WebUI
Unfortunately, it can be more difficult to get Selenium to find elements in Nuxeo’s WebUI than it would be on a simple HTML web page. In fact, if you have the Selenium IDE record your clicks while you click through the Nuxeo WebUI interface, it will essentially record each click as simply a click on the Nuxeo application without registering what specific element of the application you actually clicked on.
The reason for this is that Nuxeo makes extensive use of web components and what’s known as the Shadow DOM. The Shadow DOM refers to a separate document object model (separate from the overall page’s DOM) that is scoped to an individual web part. Although the more recent versions of web browsers all support the shadow DOM, Selenium’s recorder does not, and it requires some additional knowledge when manually writing Selenium scripts as well.
A Shadow DOM within the Shadow DOM
First of all, let’s look at an example of the Shadow DOM in use in a vanilla instance of the Nuxeo WebUI. (For the purpose of this article and its sample scripts, we are using a locally-installed version of Nuxeo with WebUI, accessible at port 80, with the DAM add-on and out-of-the-box sample content installed. A number of different programming languages can be used with Selenium. We will be using Java, and we will also be making some use of the JavaScript Executor as well, as you’ll see below, to click elements in the Shadow DOM.)
If you open up your dev tools and examine Nuxeo’s WebUI, you will see that you very quickly get to the first Shadow DOM.
So the Nuxeo application as a whole has a Shadow DOM, but the Shadow DOM in Nuxeo also contains additional Shadow DOMs within it. See the next image:
That “paper-icon-button” element is the button for Search. If we want to click that button, we need to guide Selenium through a Shadow DOM that is contained within another Shadow DOM.
Navigating the Shadow DOM(s) in Selenium
Typically, if we’re using Selenium WebDriver, we can find elements by their className or ID or other identifiers (as shown below), as long as those identifiers are unique.
If you open up your dev tools and examine Nuxeo’s WebUI, you will see that you very quickly get to the first Shadow DOM.
As of this article’s writing, this method doesn’t work for elements in the Shadow DOM. Instead, we need to use the JavascriptExecutor
to run JavaScript in the Java project and get the returned HTML element.
As an example, let’s look at how to find and click on the default search button.
To get default search button, we need to first get the element’s XPath. To do this (in Chrome), open inspect mode, and select an element in the page to inspect it, right click it and copy the full XPath as shown in the image below.
For the paper-icon-button element, its XPath is “/html/body/nuxeo-app//paper-drawer-panel/div/paper-listbox/nuxeo-menu-icon[3]//a/paper-icon-button”.
We can identify each of the shadow roots in the XPath-they are the elements before each of the double forward slashes (“//”). In this case, they are nuxeo-app and nuxeo-menu-icon.
We then need to write a small bit of JavaScript within our Java code to locate the element. To do this, we call JavascriptExecutor
and executeScript
as shown in the code snippet below. Within this, we use querySelector()
to find each of nuxeo-app and nuxeo-menu-icon. The first one, nuxeo-app, is unique, but there are several nuxeo-menu-icon elements, so we add a filter condition (in this case "[name='defaultSearch'])"
to locate it.
We end up with the following code:
WebElement searchbutton = (WebElement)((JavascriptExecutor)driver).executeScript(“return document.querySelector(“nuxeo-app”).shadowRoot.querySelector(“nuxeo-menu-icon[name=’defaultSearch’]”).shadowRoot.querySelector(“#button”)”);
searchbutton.click();
Dealing with page loads without document.readyState
The only challenge at this point is to make sure the Shadow DOM has completely loaded before we have Selenium try to find an element in the Shadow DOM. Unfortunately, waiting for document.readyState
to equal “complete” may not be enough – it seems that the Shadow DOM may still be loading sometimes even after this.
You will likely need to create a function in your code to try finding an element in the Shadow DOM for a certain number of seconds before timing out (or for a certain number of times, waiting for a certain amount of time between each attempt). For example, the function below tries for ten seconds before timing out:
public WebElement jsExecute(String str){
WebElement ele = null;
WebDriverWait wait = new WebDriverWait (driver, 10);
while(ele==null) {
wait.until(ExpectedConditions.javaScriptThrowsNoExceptions(str));
ele = (WebElement)((JavascriptExecutor)driver).executeScript(str);
}
return ele;
}
With this function, you would use the following code to execute the previous example:
WebElement searchbutton = jsExecute(“return document.querySelector(“nuxeo-app”).shadowRoot.querySelector(“nuxeo-menu-icon[name=’defaultSearch’]”).shadowRoot.querySelector(“#button”)”);
searchbutton.click();
A sample script
In a vanilla instance of Nuxeo with the DAM add-on and the default sample content, you could use the following script to log in using the default username and password and to search for an asset that you know exists in the sample content and to confirm that you’ve found it. (Take note that, before running the script, you’ll need to set the path of chromedriver.exe on line 42.)
//login-search button-River-open it
package demo;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.JavascriptExecutor;
import org.testng.annotations.Test;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
public class TestCase01 {
public WebDriver driver;
//For first load
public WebElement firstjsExecute(String str){
WebElement ele = null;
WebDriverWait wait = new WebDriverWait (driver, 20);
while(ele==null) {
wait.until(ExpectedConditions.javaScriptThrowsNoExceptions(str));
ele = (WebElement)((JavascriptExecutor)driver).executeScript(str);
}
return ele;
}
public WebElement jsExecute(String str){
WebElement ele = null;
WebDriverWait wait = new WebDriverWait (driver, 10);
while(ele==null) {
wait.until(ExpectedConditions.javaScriptThrowsNoExceptions(str));
ele = (WebElement)((JavascriptExecutor)driver).executeScript(str);
}
return ele;
}
@BeforeClass
public void beforeClass() {
System.setProperty(“webdriver.chrome.driver”,”[PATH OF chromedriver.exe]”);
driver=new ChromeDriver();
driver.manage().window().maximize();
}
@BeforeMethod
public void beforeMethod() {
driver.get(“http://localhost:8080/nuxeo”);
driver.findElement(By.id(“username”)).sendKeys(“Administrator”);
driver.findElement(By.id(“password”)).sendKeys(“Administrator”);
driver.findElement(By.name(“Submit”)).click();
//System.out.println(“login!”);
}
@Test
public void f() {
WebElement searchbutton =firstjsExecute(“return document.querySelector(“nuxeo-app”).shadowRoot.querySelector(“nuxeo-menu-icon[name=’defaultSearch’]”).shadowRoot.querySelector(“#button”)”);
searchbutton.click();
WebElement searchinput =jsExecute(“return document.querySelector(“nuxeo-app”).shadowRoot.querySelector(“nuxeo-search-form[name=’defaultSearch’]”).shadowRoot.querySelector(“nuxeo-search-form-layout”).shadowRoot.querySelector(“nuxeo-layout”).shadowRoot.querySelector(“nuxeo-default-search-form”).shadowRoot.querySelector(“nuxeo-input”).shadowRoot.querySelector(“paper-input”).shadowRoot.querySelector(“input”)”);
searchinput.sendKeys(“Rivern”);
WebElement searchresult =jsExecute(“return document.querySelector(“nuxeo-app”).shadowRoot.querySelector(“nuxeo-search-page”).shadowRoot.querySelector(“nuxeo-results-view”).shadowRoot.querySelector(“nuxeo-search-results-layout”).shadowRoot.querySelector(“nuxeo-layout”).shadowRoot.querySelector(“nuxeo-default-search-results”).shadowRoot.querySelector(“nuxeo-document-list-item”)”);
searchresult.click();
Assert.assertEquals(driver.getCurrentUrl(),”http://localhost:8080/nuxeo/ui/#!/browse/default-domain/workspaces/Sample%20Content/Videos/River.mp4″);
}
@AfterMethod
public void afterMethod() {
}
//driver.close() is commented out below so that you have time to see the result visually. Uncomment it to automatically close the browser at the conclusion of the test.
@AfterClass
public void afterClass() {
//driver.close();
}
}
Want more information?
iSoftStone has deep experience around Content Services Platforms. You can learn more about Nuxeo there and on Nuxeo’s own website.