Akkordeon-Menü mit Selenium

Beim Umgang mit einem Akkordeon-Menu (hier umgesetzt mit jquery – das Beispiel befindet sich am Ende des Artikels: http://viralpatel.net/blogs/create-accordion-menu-jquery/) ergibt sich bei der Testautomation das Problem, dass ich nicht einfach kodieren kann:
1.) Clicke auf „Technology“-Link.
2.) Clicke auf „Twitter“-Link.
um zur entsprechenden Seite zu gelangen. In einem komplexeren Testfall, bei dem ich vielmals mit dem Akkordeon-Menü arbeite, kommt es vor, dass das Technology-Menü bereits geöffnet ist. Dann würde ein Klicken das Menü wieder schließen und ich käme an den Twitter-Link nicht mehr ran – mein Testfall würde mit einer ElementNotVisibleException über den Jordan gehen. Daher müssen wir noch eine kleine Abfrage einbauen:
1.) Falls ein das Technology-Menü nicht geöffnet ist (=“Twitter“-Link nicht da ist), öffne es.
2.) Clicke auf Twitter-Link.

 

Abfragen, ob ein bestimmtes Element existiert, funktionieren im Webdriver nur indirekt, wie hier ausgeführt: http://stackoverflow.com/questions/6521270/webdriver-check-if-an-element-exists Um die Diskussion zusammen zu fassen (und zu ergänzen):
1.) Die eleganteste Lösung die Existenz eines Elements zu prüfen ist
driver.findElements( By.id("...") ).size() != 0
2.) Wenn man mit implicit wait arbeitet (wie im Folgenden angenommen), dann sollte man den Sekundenwert in einer Variable standardSecondsToWait speichern und zwar dort, wo man den implicit wait initial festlegt:
int standardSecondsToWait = 30;
driver.manage().timeouts().implicitlyWait(standardSecondsToWait, TimeUnit.SECONDS);

3.) Der driver wartet die komplette Zeit des implicit wait, bis er endlich entscheidet, dass ein tatsächlich nicht vorhandenes Element auch tatsächlich nicht vorhanden ist. Erst dann entscheidet er, dass die o.g. Bedingung falsch ist und erst dann fährt er mit der Programmausführung fort. Bei mehreren solcher Prüfungen in einem Testcase und entsprechender Länge des implicit wait verschwenden wir kostbare Testzeit.
4.) Eine good practice ist es daher, die Bedingung isoliert und innerhalb von einer „implicit wait-Klammer“ abzufrühstücken:


driver.manage().timeouts().implicitlyWait(0, TimeUnit.SECONDS);
boolean exists = driver.findElements( By.id("...") ).size() != 0
driver.manage().timeouts().implicitlyWait(standardSecondsToWait, TimeUnit.SECONDS);
if (exists) {do something} (...)

Konsequenterweise umschließt die „implicit wait-Klammer“ nur die Abfrage dieser einen Bedinungung und nicht mehr, denn wenn man ein ausgesprochener Fan der implicit waits ist (iGs zu den Fans der explicit waits), dann sollte man auch nur im notwendigen Fall davon abweichen.

Am Ende des ersten Absatzes sprach ich von der Prüfung, ob der „Twitter“-Link da ist. Der zweite Absatz handelt von der Prüfung, ob ein Element existiert. Genau gesagt geht es jedoch darum, ob ein Element im Akkordeon-Menü (im Beispiel der „Twitter“-Link) sichtbar ist. Die Prüfung auf Existenz (also ob der Knoten im DOM tree angelegt ist) greift zu kurz, denn ein existierendes Element könnte vom stylesheet noch display:none rangeklatscht bekommen: es existiert, ist jedoch nicht sichtbar. Genau das macht aber das Akkordeon-Menü aus: nur die Links des geöffneten Submenüs sind sichtbar – die Links der anderen Submenüs existieren zwar, sind jedoch unsichtbar (display:none). WebDriver quittiert den Klickversuch dieser unsichtbaren Links mit einer ElementNotVisibleException (gemäß dem Anspruch einen menschlichen User zu simulieren). Wir müssen den Ansatz im zweiten Absatz also noch anpassen:

driver.manage().timeouts().implicitlyWait(0, TimeUnit.SECONDS);
        Boolean isOffen = driver.findElement(By.xpath("//a[contains(text(),'Football')]")).isDisplayed();
        driver.manage().timeouts().implicitlyWait(standardSecondsToWait, TimeUnit.SECONDS);

Achtung: Würde ich hier mittels linkText() oder partialLinkText() suchen, statt mir xpath(), würde dieser Code nicht funktionieren. Diese beiden Methoden erfordern einen sichtbaren Text und wenn sie diesen nicht finden, brechen Sie den Testlauf mit „NoSuchElement“ ab (http://www.seleniumhq.org/docs/03_webdriver.html#by-link-text)

Und hier der vollständige Code:

package de.it-kosmopolit.meinblog;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.TimeUnit;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.internal.Locatable;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.testng.annotations.Test;

public class Accordeon {

    @Test
    public void testAccordeon() throws InterruptedException, MalformedURLException {

        String username = "it-kosmopolit";
        String accessKey = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
        String url = "http://" + username + ":" + accessKey + "@ondemand.saucelabs.com:80/wd/hub";

        DesiredCapabilities cap = DesiredCapabilities.firefox(); //Browserdefaults auswählen
        cap.setCapability("version", "15"); //Version des Browsers festlegen
        cap.setCapability("platform", "Windows 2008"); //Betriebssystem festlegen
        cap.setCapability("username", username); //Mein Username bei SauceLabs
        cap.setCapability("accessKey", accessKey); //Mein Access-key bei SauceLabs
        WebDriver driver = new RemoteWebDriver(new URL(url), cap);

        int standardSecondsToWait = 30;
        driver.manage().timeouts().implicitlyWait(standardSecondsToWait, TimeUnit.SECONDS);
        driver.get("http://viralpatel.net/blogs/create-accordion-menu-jquery/");

        // das Fenster an die Position scrollen, mit der man das "Schauspiel" am besten sieht
        // Code-Quelle: http://stackoverflow.com/a/9852744
        Locatable hoverItem = (Locatable) driver.findElement(By.xpath("//h2[contains(text(),'Online Demo')]"));
        int y = hoverItem.getCoordinates().getLocationOnScreen().getY();
        ((JavascriptExecutor) driver).executeScript("window.scrollBy(0," + y + ");");
        Thread.sleep(2000); //nur für's "Schauspiel"

        //das Beispiels-Akkordeon befindet sich in einem namenlosen iframe ...
        WebElement properFrame = driver.findElement(By.xpath("//iframe/preceding-sibling::br/following-sibling::iframe"));
        driver.switchTo().frame(properFrame);

        driver.manage().timeouts().implicitlyWait(0, TimeUnit.SECONDS);
        Boolean isOffen = driver.findElement(By.xpath("//a[contains(text(),'Football')]")).isDisplayed();
        driver.manage().timeouts().implicitlyWait(standardSecondsToWait, TimeUnit.SECONDS);

        //Das Sports-Menü ist initial tatsächlich geöffnet.
        //Ohne die Prüfung würde es hier also knallen (ElementNotVisibleException)!
        if (isOffen) {
            driver.findElement(By.xpath("//a[contains(text(),'Football')]")).click();
            Thread.sleep(2000); //nur für's "Schauspiel"
        } else {
            driver.findElement(By.xpath("//li[contains(text(),'Sports')]")).click();
            driver.findElement(By.xpath("//a[contains(text(),'Football')]")).click();
        }

        driver.manage().timeouts().implicitlyWait(0, TimeUnit.SECONDS);
        Boolean isOffen2 = driver.findElement(By.xpath("//a[contains(text(),'Twitter')]")).isDisplayed();
        driver.manage().timeouts().implicitlyWait(standardSecondsToWait, TimeUnit.SECONDS);

        if (isOffen2) {
            driver.findElement(By.xpath("//a[contains(text(),'Twitter')]")).click();
        } else {
            driver.findElement(By.xpath("//li[contains(text(),'Technology')]")).click();
            Thread.sleep(2000); //nur für's "Schauspiel"
            driver.findElement(By.xpath("//a[contains(text(),'Twitter')]")).click();
            Thread.sleep(2000); //nur für's "Schauspiel"
        }

        driver.quit();
    }
}

Und hier der Testcase zum Anschauen: https://saucelabs.com/tests/e7cdafe4389b4585867399af71907ea2#
(Hinweis: die Links sind „blinde“ Demolinks – führen also niergens hin, im Video erkennt man das Klicken an einer Farbänderung des Links von blau zu rot)