Selenium - Shadow DOM und Shadow Root
08. Februar 2020 Softwaretest von Eric Kubenka
Als ich in der jüngeren Vergangenheit eine moderne Webseite testen sollte, stand ich vor dem Problem, dass die dort existierenden Elemente mittels Shadow DOM gerendert wurden.
Shadow DOM Elemente sind aus dem Selenium / Automatisierungsumfeld betrachtet Frames sehr ähnlich und halten einige Falltüren bereit.
Ich zeige, wie man solche Elemente mittels Selenium lokalisiert und damit arbeitet.
Entwicklungsumgebung einrichten
Ich arbeite mit Windows 10 und verwende chocolatey
als Paketverwaltung, weil es das Reproduzieren von Software-Installationen einfacher macht, weshalb ich kurz darstellen möchte, wie ich meine Entwicklungsumgebung inklusive Java, JDK, Maven und ChromeDriver aufgesetzt habe.
choco install chromedriver
choco install maven
choco install openjdk --version=12.0.2
Website mit Shadow Roots
Extra für diesen Post hoste ich hier auf meinem Blog eine Demo-Seite, welche mit Shadow DOM arbeitet.
code-fever: Example for Shadow DOM
Test erstellen
Aus persönlicher Überzeugung verwende ich in all meinen Testprojekten TestNG
, weshalb ich dementsprechend nun kurz zeige, wie ich einen Basis TestNG Testfall mit Selenium WebDriver
erzeuge und verwende. Dabei sollte beachtet werden, dass in produtkiven Projekten stets ein Manager oder Ähnliches für das Handling von WebDriver-Sessions verwendet wird, anstatt es via BeforeMethod
und AfterMethod
zu lösen.
public class ShadowRootExampleTest {
private WebDriver driver = null;
@BeforeMethod(alwaysRun = true)
public void openDriver() {
driver = new ChromeDriver();
driver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS);
}
@AfterMethod(alwaysRun = true)
public void closeDriver() {
if (driver != null) {
driver.close();
}
}
@Test
public void testShadowRoot() {
driver.get("https://examples.code-fever.de/2020/selenium-shadow-root/demosite/");
}
}
Elemente vom Shadow DOM lokalisieren
Um Elemente eines Shadow DOM mittels Selenium zu lokalisieren, muss man zuerst den Shadow-Root
finden, also jenes Element, welches den Beginn des Shadow DOM darstellt. In dem Beispielfall ist dies #shadow-host
.
Ab dieser Ebene ist es mit Standard-Selenium-Selektoren nicht mehr möglich tiefere Elemente zu lokalisieren. Dazu ein Beispiel, welches zu einer ElementNotFoundException
führen würde.
final WebElement shadowContentText = driver.findElement(By.cssSelector("#shadow-content p"));
Um dieses Problem zu umgehen ist die Lösung der JavaScritpExecutor
, mit welchem ein neues Element ermittelt werden kann.
final WebElement shadowHost = driver.findElement(By.cssSelector("#shadow-host"));
final JavascriptExecutor javascriptExecutor = (JavascriptExecutor) driver;
final WebElement shadowRoot = (WebElement) javascriptExecutor.executeScript("return arguments[0].shadowRoot", shadowHost);
Ausgehend von diesem WebElement
können nun mittels findElement()
die tiefer liegenden Elemente lokalisiert werden. Mit einer kleinen Ausnahme!
Nur findElement(By.cssSelector()) funktioniert!
Die Ausnahme ist, dass tiefer liegende Elemente nur mittels css-Selektor lokalisiert werden können, da es technisch einfach nicht möglich ist, einen anderen Ausdruck zu verwenden. Doch auch für dieses "Problem" gibt es eine einfache Lösung. Die meisten Shadow DOMs werden mit einem Root-Element beginnen, welches den Inhalt des DOM wrappt. Ausgehend von diesem Element, wenn es denn einmal lokalisiert wurde, kann dann wieder jede Such-Methode verwendet werden, um Elemente zu lokalisieren.
Funktioniert, da CSS-Selektor:
final WebElement shadowContentText = shadowRoot.findElement(By.cssSelector("#shadow-content p"));
Funktioniert nicht, da Xpath-Selektor:
final WebElement shadowContentTextWithXpath = shadowRoot.findElement(By.xpath("//*[@id='shadow-content']//p"));
Funktioniert, da zuerst ein CSS-Selektor verwendet wird:
final WebElement shadowContent = shadowRoot.findElement(By.cssSelector("#shadow-content"));
final WebElement shadowContentTextWithXpath = shadowContent.findElement(By.xpath("//p"));
Abschluss
Alle Source-Files und eine funktionierende Demo-Seite gibt es wie immer auf GitHub
.
Links
Internal:
External: