Using Selenium with Nuxeo and the Shadow DOM

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.

Partial screenshot from the Elements tab in Chrome dev tools, examining Nuxeo's WebUI. The head tag is closed. The body tag is open. Under the body tag, the nuxeo-connection tag is closed, and the nuxeo-app tag is open. Under the nuxeo-app tag, the shadow root tag is open.

Figure 1: In this image, we are in the inspector, and we have expanded the <body> tag and the <nuxeo-app…> tag. Right below that, we have expanded the #shadow-root.

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:

Partial screenshot from the Elements tab in Chrome dev tools, examining Nuxeo's WebUI. A nuxeo-menu-icon tag is open. It contains properties including a name property of defaultSearch and icon property of nuxeo colon search. Under this element, the shadow-root tag is open. Under that, the A tag is open, and under that, there is a paper-icon-button element that contains properties including id of button.

Figure 2: In this image, we are already within the first Shadow DOM, and we have expanded element <nuxeo-menu-icon…>, and we see that there is a #shadow-root for this element. Contained within this Shadow DOM, we see that that there is a <paper-icon-button...> element.

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.

Partial screenshot showing that a developer has typed driver period findElement parentheses by period, and the IDE is providing a variety of options such as className, cssSelector, id, linkText, etc to go after the by period.
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.

Partial screenshot from the Elements tab in Chrome dev tools, examining Nuxeo's WebUI. The developer has right-clicked on the paper-icon-button element, which was also shown in a previous screenshot, and has hovered over Copy in the menu, and over Copy full XPath in the submenu.
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.

Man and woman meeting in a bright and modern office. Whiteboard with writing and sticky notes is behind them.

Engage your digital transformation.

The right technology partner gets you where your customers expect you to be.

Contact us