All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opennms.smoketest.selenium.AbstractOpenNMSSeleniumHelper Maven / Gradle / Ivy

/*
 * Licensed to The OpenNMS Group, Inc (TOG) under one or more
 * contributor license agreements.  See the LICENSE.md file
 * distributed with this work for additional information
 * regarding copyright ownership.
 *
 * TOG licenses this file to You under the GNU Affero General
 * Public License Version 3 (the "License") or (at your option)
 * any later version.  You may not use this file except in
 * compliance with the License.  You may obtain a copy of the
 * License at:
 *
 *      https://www.gnu.org/licenses/agpl-3.0.txt
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied.  See the License for the specific
 * language governing permissions and limitations under the
 * License.
 */
package org.opennms.smoketest.selenium;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.openqa.selenium.support.ui.ExpectedConditions.elementToBeClickable;
import static org.openqa.selenium.support.ui.ExpectedConditions.visibilityOfElementLocated;

import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.commons.io.FileUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.jsoup.Jsoup;
import org.jsoup.select.Elements;
import org.junit.Rule;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.openqa.selenium.Alert;
import org.openqa.selenium.By;
import org.openqa.selenium.Dimension;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.Keys;
import org.openqa.selenium.NoAlertPresentException;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.Point;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.Select;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.rnorth.ducttape.unreliables.Unreliables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

@SuppressWarnings("java:S2068")
public abstract class AbstractOpenNMSSeleniumHelper {
    private static final Logger LOG = LoggerFactory.getLogger(AbstractOpenNMSSeleniumHelper.class);

    public static final long   LOAD_TIMEOUT       = Long.getLong("org.opennms.smoketest.web-timeout", 120000l);
    public static final long   REQ_TIMEOUT        = Long.getLong("org.opennms.smoketest.requisition-timeout", 240000l);

    public static final String BASIC_AUTH_USERNAME = "admin";
    public static final String BASIC_AUTH_PASSWORD = "admin";

    // username/password combination that triggers the password gate, which prompts
    // user to change the default "admin" password
    public static final String PASSWORD_GATE_USERNAME = "admin";
    public static final String PASSWORD_GATE_PASSWORD = "admin";

    public static final String REQUISITION_NAME   = "SeleniumTestGroup";
    public static final String USER_NAME          = "SmokeTestUser";
    public static final String GROUP_NAME         = "SmokeTestGroup";

    public static final File DOWNLOADS_FOLDER = new File("target/downloads");

    public WebDriverWait wait = null;
    public WebDriverWait requisitionWait = null;

    public abstract WebDriver getDriver();
    public abstract String getBaseUrlInternal();
    public abstract String getBaseUrlExternal();

    @Rule
    public TestWatcher m_watcher = new TestWatcher() {
        @Override
        protected void starting(final Description description) {
            LOG.debug("Using driver: {}", getDriver());
            try {
                setImplicitWait();
            } catch (WebDriverException e) {
                e.printStackTrace();
            }
            getDriver().manage().window().setPosition(new Point(0,0));
            getDriver().manage().window().maximize();
            wait = new WebDriverWait(getDriver(), Duration.ofMillis(LOAD_TIMEOUT));
            requisitionWait = new WebDriverWait(getDriver(), Duration.ofMillis(REQ_TIMEOUT));

            login();

            // make sure everything's in a good state if possible
            cleanUp();
        }

        @Override
        protected void failed(final Throwable e, final Description description) {
            final String testName = description.getMethodName();
            final WebDriver driver = getDriver();

            if (driver == null) {
                LOG.warn("Test {} failed... no web driver was set.", testName);
                return;
            }

            LOG.debug("Test {} failed... attempting to take screenshot.", testName);

            // Reset the implicit wait since we can't trust the last value
            driver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS);

            if (driver instanceof TakesScreenshot) {
                final TakesScreenshot shot = (TakesScreenshot)driver;

                try {
                    final Path from = shot.getScreenshotAs(OutputType.FILE).toPath();
                    final Path to = Paths.get("target", "screenshots", description.getClassName() + "." + testName + ".png");
                    LOG.debug("Screenshot saved to: {}", from);

                    try {
                        Files.createDirectories(to.getParent());
                        Files.move(from, to, StandardCopyOption.REPLACE_EXISTING);
                        LOG.debug("Screenshot moved to: {}", to);
                    } catch (final IOException ioe) {
                        LOG.debug("Failed to move screenshot from {} to {}", from, to, ioe);
                    }
                } catch (final Exception sse) {
                    LOG.debug("Failed to take screenshot.", sse);
                }
            } else {
                LOG.debug("Driver can't take screenshots.");
            }

            try {
                LOG.debug("Attempting to dump DOM.");
                final String domText = driver.findElement(By.tagName("html")).getAttribute("innerHTML");
                final Path to = Paths.get("target", "contents", description.getClassName() + "." + testName + ".html");

                try {
                    Files.createDirectories(to.getParent());
                    Files.write(to, domText.getBytes(StandardCharsets.UTF_8));
                    LOG.debug("Wrote DOM to {}", to);
                } catch (final Exception eDOMfile) {
                    LOG.warn("Failed to dump DOM to {}", to, eDOMfile);
                }
            } catch (final Exception eDOM) {
                LOG.debug("Failed to dump DOM: {}", eDOM.getMessage(), eDOM);
            }
            LOG.debug("Current URL: {}", getDriver().getCurrentUrl());
        }

        @Override
        protected void finished(final Description description) {
            final String testName = description.getMethodName();
            LOG.debug("Test {} finished.", testName);
            cleanUp();
        }

        protected void cleanUp() {
            LOG.debug("=== cleanup starting ===");
            try {
                deleteTestRequisition();
                deleteTestUser();
                deleteTestGroup();
                LOG.debug("=== cleanup complete ===");
            } catch (final Exception e) {
                LOG.error("Cleaning up failed. Future tests will be in an unhandled state.", e);
            }
        }
    };

    public WebDriver.Timeouts setImplicitWait() {
        return setImplicitWait(LOAD_TIMEOUT, TimeUnit.MILLISECONDS);
    }

    public WebDriver.Timeouts setImplicitWait(final long time, final TimeUnit unit) {
        LOG.trace("Setting implicit wait to {} milliseconds.", unit.toMillis(time));
        return getDriver().manage().timeouts().implicitlyWait(time, unit);
    }

    protected WebDriverWait waitFor(final long seconds) {
        return new WebDriverWait(getDriver(), Duration.ofSeconds(seconds));
    }

    protected void waitForClose(final By selector) {
        LOG.debug("waitForClose: {}", selector);
        try {
            setImplicitWait(1, TimeUnit.SECONDS);
            wait.until(new ExpectedCondition() {
                @Override
                public Boolean apply(final WebDriver input) {
                    try {
                        sleepQuietly(200);
                        final List elements = input.findElements(selector);
                        if (elements.size() == 0) {
                            return true;
                        }
                        LOG.debug("waitForClose: matching elements: {}", elements);
                        WebElement element = input.findElement(selector);
                        final Point location = element.getLocation();
                        // recreate it because the browser is funny about timing after the getLocation()
                        element = input.findElement(selector);
                        final Dimension size = element.getSize();
                        if (new Point(0,0).equals(location) && new Dimension(0,0).equals(size)) {
                            LOG.debug("waitForClose: {} element technically exists, but is sized 0,0", element);
                            return true;
                        }
                        LOG.debug("waitForClose: {} element still exists at location {} with size {}: {}", selector, location, size, element.getText());
                        return false;
                    } catch (final NoSuchElementException | StaleElementReferenceException e) {
                        return true;
                    } catch (final Exception e) {
                        LOG.debug("waitForClose: unknown exception", e);
                        throw new OpenNMSTestException(e);
                    }
                }
            });
        } finally {
            setImplicitWait();
        }
    }

    /**
     * Standard login for smoke tests. Navigate to login page, log in as default admin user,
     * skip the password gate page and then close the Usage Statistics Sharing dialog if present.
     */
    public void login() {
        login(BASIC_AUTH_USERNAME, BASIC_AUTH_PASSWORD, true, true, true, false);
    }

    /**
     * Perform a login with given credentials and various options.
     *
     * If login username/password is "admin/admin", this results in going to the Admin Password Gate page.
     * 'skip' will skip having to change the password and continue to the main page.
     *
     * @param username Username to use for the login
     * @param password Password to use for the login
     * @param skip If true, clicks Skip on the password gate page to avoid having to change the admin password
     * @param closeUsageStatsSharing If true, close the Usage Statistics Sharing dialog if it appears
     * @param navigateToLoginPage If true, navigate to the login page. Set to false to navigate
     *     to a different page before calling this method, e.g. to test that redirect after login works.
     */
    public void login(final String username, final String password, final boolean skip, final boolean closeUsageStatsSharing,
                      final boolean navigateToLoginPage, final boolean skipCookieDeletion) {
        LOG.debug("Login: username={}, skip={}, closeUsageStatsSharing={}, navigateToLoginPage={}, skipCookieDeletion={}",
            username, skip, closeUsageStatsSharing, navigateToLoginPage, skipCookieDeletion);

        if (navigateToLoginPage) {
            if (!skipCookieDeletion) {
                // Start with a clean slate
                getDriver().manage().deleteAllCookies();
            }

            getDriver().get(getBaseUrlInternal() + "opennms/login.jsp");
        }

        waitForLogin();

        enterText(By.name("j_username"), username);
        enterText(By.name("j_password"), password);
        clickElement(By.name("Login"));

        wait.until((WebDriver driver) -> {
            return ! driver.getCurrentUrl().contains("login.jsp");
        });

        // bootstrap header, exists in all JSP-based OpenNMS pages
        wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//div[@id='content']")));

        ensureLoginSuccess();

        if (skip) {
            skipPasswordGate(username, password);
        }

        if (closeUsageStatsSharing) {
            closeUsageStatisticsSharingDialog();
        }
    }

    private void invokeWithImplicitWait(int implicitWait, Runnable runnable) {
        Objects.requireNonNull(runnable);
        try {
            // Disable implicitlyWait
            setImplicitWait(Math.max(0, implicitWait), TimeUnit.MILLISECONDS);
            runnable.run();
        } finally {
            setImplicitWait();
        }
    }

    protected void logout() {
        LOG.debug("Logout started");
        getElementWithoutWaiting(By.name("headerLogoutForm")).submit();
        waitForLogin();
        LOG.debug("Logout complete");
    }

    private void waitForLogin() {
        // Wait until the login form is complete
        wait.until(ExpectedConditions.visibilityOfElementLocated(By.name("j_username")));
        wait.until(ExpectedConditions.visibilityOfElementLocated(By.name("j_password")));
        wait.until(ExpectedConditions.elementToBeClickable(By.name("Login")));
    }

    protected ExpectedCondition pageContainsText(final String text) {
        return org.opennms.smoketest.selenium.ExpectedConditions.pageContainsText(text);
    }

    public void focusElement(final By by) {
        sleepQuietly(200);
        waitForElement(by).click();
    }

    public void clearElement(final By by) {
        sleepQuietly(200);
        waitForElement(by).clear();
    }

    protected void ensureLoginSuccess() {
        invokeWithImplicitWait(0, () -> {
            try {
                // Make sure that the 'login-attempt-failed' element is not present
                findElementById("login-attempt-failed");
                fail("Login failed: " + findElementById("login-attempt-failed-reason").getText());
            } catch (NoSuchElementException e) {
                // This is expected
            }
        });
    }

    /**
     * Logging in with "admin/admin" will result in navigating to the passwordGate page,
     * this clicks "Skip" to continue.
     */
    protected void skipPasswordGate(final String username, final String password) {
        if (username.equals(PASSWORD_GATE_USERNAME) && password.equals(PASSWORD_GATE_PASSWORD)) {
            clickElement(By.id("btn_skip"));

            wait.until((WebDriver driver) -> {
                return !driver.getCurrentUrl().contains("passwordGate.jsp");
            });
        }
    }

    /**
     * Close the Usage Statistics Sharing dialog if present.
     */
    protected void closeUsageStatisticsSharingDialog() {
        invokeWithImplicitWait(0, () -> {
            try {
                WebElement element = findElementById("usage-statistics-sharing-modal");

                if (element.isDisplayed()) { // usage statistics modal is visible
                    findElementById("usage-statistics-sharing-notice-dismiss").click(); // close modal
                }
            } catch (NoSuchElementException e) {
                // "usage-statistics-sharing-notice-dismiss" is not visible or does not exist.
                // No further action required
            }
        });
    }

    public void assertElementDoesNotExist(final By by) {
        LOG.debug("assertElementDoesNotExist: {}", by);
        WebElement element = getElementWithoutWaiting(by);
        if (element == null) {
            LOG.debug("Success: element does not exist: {}", by);
            return;
        }
        throw new OpenNMSTestException("Element should not exist, but was found: " + element);
    }

    public WebElement getElementImmediately(final By by) {
        WebElement element = null;
        try {
            setImplicitWait(0, TimeUnit.MILLISECONDS);
            element = getDriver().findElement(by);
        } catch (final NoSuchElementException e) {
            return null;
        } finally {
            setImplicitWait();
        }
        return element;
    }

    protected WebElement getElementWithoutWaiting(final By by) {
        WebElement element = null;
        try {
            setImplicitWait(2, TimeUnit.SECONDS);
            element = getDriver().findElement(by);
        } catch (final NoSuchElementException e) {
            return null;
        } finally {
            setImplicitWait();
        }
        return element;
    }

    protected void assertElementDoesNotHaveText(final By by, final String text) {
        LOG.debug("assertElementDoesNotHaveText: locator={}, text={}", by, text);
        WebElement element = null;
        try {
            setImplicitWait(2, TimeUnit.SECONDS);
            element = getDriver().findElement(by);
            assertTrue(!element.getText().contains(text));
        } catch (final NoSuchElementException e) {
            LOG.debug("Success: element does not exist: {}", by);
            return;
        } finally {
            setImplicitWait();
        }
    }

    protected void assertElementHasText(final By by, final String text) {
        LOG.debug("assertElementHasText: locator={}, text={}", by, text);
        WebElement element = waitForElement(by);
        assertTrue(element.getText().contains(text));
    }

    protected String handleAlert() {
        return handleAlert(null);
    }

    protected String handleAlert(final String expectedText) {
        LOG.debug("handleAlert: expectedText={}", expectedText);
        try {
            final Alert alert = getDriver().switchTo().alert();
            final String alertText = alert.getText();
            if (expectedText != null) {
                assertEquals(expectedText, alertText);
            }
            alert.dismiss();
            return alertText;
        } catch (final NoAlertPresentException e) {
            LOG.debug("handleAlert: no alert is active");
        } catch (final TimeoutException e) {
            LOG.debug("handleAlert: no alert was found");
        }
        return null;
    }

    protected void setChecked(final By by) {
        LOG.debug("setChecked: locator={}", by);
        final var element = scrollToElement(by);
        if (element.isSelected()) {
            return;
        } else {
            element.click();
        }
    }

    protected void setUnchecked(final By by) {
        LOG.debug("setUnchecked: locator={}", by);
        final var element = scrollToElement(by);
        if (element.isSelected()) {
            element.click();
        } else {
            return;
        }
    }

    protected void clickMenuItem(final String menuItemText, final String submenuItemText, final String submenuItemHref) {
        clickMenuItem(menuItemText, submenuItemText, submenuItemHref, 30);
    }

    protected void clickMenuItem(final String menuItemText, final String submenuItemText, final String submenuItemHref, int timeout) {
        LOG.debug("clickMenuItem: itemText={}, submenuItemText={}, submenuHref={}, timeout={}", menuItemText, submenuItemText, submenuItemHref, timeout);

        if (timeout == 0) {
            timeout = 30;
        }

        // Repeat the process altering the offset slightly everytime
        final AtomicInteger offset = new AtomicInteger(10);
        final WebDriverWait shortWait = new WebDriverWait(getDriver(), Duration.ofSeconds(1));
        Unreliables.retryUntilSuccess(timeout, TimeUnit.SECONDS, () -> {
            final Actions action = new Actions(getDriver());

            final WebElement menuElement;
            if (menuItemText.startsWith("name=")) {
                final String menuItemName = menuItemText.replaceFirst("name=", "");
                menuElement = findElementByName(menuItemName);
            } else {
                menuElement = findElementByXpath("//a[contains(text(), '" + menuItemText + "')]");
            }
            action.moveToElement(menuElement, offset.get(), offset.get()).perform();
            if (offset.incrementAndGet() > 10) {
                offset.set(0);
            }

            final WebElement submenuElement;
            if (submenuItemText != null) {
                if (submenuItemHref == null) {
                    submenuElement = findElementByXpath("//a[contains(text(), '" + submenuItemText + "')]");
                } else {
                    submenuElement = findElementByXpath("//a[contains(@href, '" + submenuItemHref + "') and contains(text(), '" + submenuItemText + "')]");
                }
            } else {
                submenuElement = null;
            }

            if (submenuElement == null) {
                // no submenu given, just click the main element
                // wait until the element is visible, not just present in the DOM
                shortWait.until(ExpectedConditions.visibilityOf(menuElement));
                menuElement.click();
            } else {
                // we want a submenu item, click it instead
                // wait until the element is visible, not just present in the DOM
                shortWait.until(ExpectedConditions.visibilityOf(submenuElement));
                submenuElement.click();
            }
            return null;
        });
    }

    protected void frontPage() {
        LOG.debug("navigating to the front page");
        getDriver().get(getBaseUrlInternal() + "opennms/");
        getDriver().findElement(By.id("index-contentmiddle"));
    }

    public void adminPage() {
        LOG.debug("navigating to the admin page");
        getDriver().get(getBaseUrlInternal() + "opennms/admin/index.jsp");
    }

    protected void nodePage() {
        LOG.debug("navigating to the node page");
        getDriver().get(getBaseUrlInternal() + "opennms/element/nodeList.htm");
    }

    protected void notificationsPage() {
        LOG.debug("navigating to the notifications page");
        getDriver().get(getBaseUrlInternal() + "opennms/notification/index.jsp");
    }

    protected void outagePage() {
        LOG.debug("navigating to the outage page");
        getDriver().get(getBaseUrlInternal() + "opennms/outage/index.jsp");
    }

    protected void provisioningPage() {
        LOG.debug("navigating to the provisioning page");
        getDriver().get(getBaseUrlInternal() + "opennms/admin/index.jsp");
        getDriver().findElement(By.linkText("Manage Provisioning Requisitions")).click();
    }

    protected void reportsPage() {
        LOG.debug("navigating to the reports page");
        getDriver().get(getBaseUrlInternal() + "opennms/report/index.jsp");
    }

    protected void searchPage() {
        LOG.debug("navigating to the search page");
        getDriver().get(getBaseUrlInternal() + "opennms/element/index.jsp");
    }

    protected void supportPage() {
        LOG.debug("navigating to the support page");
        getDriver().get(getBaseUrlInternal() + "opennms/support/index.jsp");
    }

    protected void goBack() {
        LOG.warn("hitting the 'back' button");
        getDriver().navigate().back();
    }

    public WebElement clickElement(final By by) {
        return waitUntil(new Callable() {
            @Override public WebElement call() throws Exception {
                final WebElement el = getElementImmediately(by);
                el.click();
                return el;
            }
        });
    }

    public WebElement waitForElement(final By by) {
        return waitForElement(wait, by);
    }

    public WebElement waitForElement(final WebDriverWait w, final By by) {
        return waitUntil(null, w, new Callable() {
            @Override public WebElement call() throws Exception {
                final WebElement el = getDriver().findElement(by);
                if (el.isDisplayed() && el.isEnabled()) {
                    return el;
                }
                return null;
            }
        });
    }

    public void enterAutocompleteText(final By textInput, final String text) {
        waitUntil(100L, null, new Callable() {
            @Override public Boolean call() throws Exception {
                waitForElement(textInput).clear();
                waitForElement(textInput).click();
                waitForElement(textInput).sendKeys(Keys.chord(Keys.CONTROL, "a"), Keys.BACK_SPACE);
                waitForValue(textInput, "");
                waitForElement(textInput).sendKeys(text);
                // Click on the item that appears
                findElementByXpath("//span[text()='" + text + "']").click();
                return true;
            }
        });
    }

    public void clickUntilVaadinPopupAppears(final By by, final String title) {
        waitUntil(100L, null, new Callable() {
            @Override public Boolean call() throws Exception {
                final WebDriver driver = getDriver();
                WebElement popup = getVaadinPopup(driver, title);

                if (popup == null) {
                    try {
                        LOG.debug("clickUntilVaadinPopupAppears: looking for '{}'", by);
                        final WebElement el = getElementImmediately(by);
                        if (el == null) {
                            LOG.debug("clickUntilVaadinPopupAppears: element not found: {}", by);
                            sleepQuietly(50);
                            return false;
                        } else {
                            LOG.debug("clickUntilVaadinPopupAppears: clicking element: {}", el);
                            el.click();
                            sleepQuietly(50);
                        }
                    } catch (final Throwable t) {
                        LOG.debug("clickUntilVaadinPopupAppears: exception raised while attempting to click {}", by, t);
                        return false;
                    }

                    popup = getVaadinPopup(driver, title);
                    if (popup != null) {
                        return true;
                    }
                } else if (popup.isDisplayed() && popup.isEnabled()) {
                    return true;
                } else {
                    LOG.debug("clickUntilVaadinPopupAppears: popup with title '{}' is gone", title);
                }
                return false;
            }
        });
        waitFor(1);
    }

    public void clickUntilVaadinPopupDisappears(final By by, final String title) {
        waitUntil(100L, null, new Callable() {
            @Override public Boolean call() throws Exception {
                final WebDriver driver = getDriver();
                WebElement popup = getVaadinPopup(driver, title);

                if (popup != null) {
                    try {
                        LOG.debug("clickIdUntilVaadinPopupDisappears: looking for '{}'", by);
                        final WebElement el = getElementImmediately(by);
                        if (el == null) {
                            LOG.debug("clickIdUntilVaadinPopupDisappears: element not found: {}", by);
                            sleepQuietly(50);
                            return false;
                        } else {
                            LOG.debug("clickIdUntilVaadinPopupDisappears: clicking element: {}", el);
                            el.click();
                            sleepQuietly(50);
                        }
                    } catch (final Throwable t) {
                        LOG.debug("clickUntilVaadinPopupDisappears: exception raised while attempting to click {}", by, t);
                        return false;
                    }

                    popup = getVaadinPopup(driver, title);
                    if (popup == null) {
                        return true;
                    }
                } else {
                    return true;
                }
                return false;
            }
        });
        waitFor(1);
    }

    protected boolean inVaadin() {
        try {
            final WebElement element = getElementImmediately(By.className("v-generated-body"));
            if (element != null) {
                return true;
            }
        } catch (final Exception e) {
        }
        return false;
    }

    protected void selectVaadinFrame() {
        if (!inVaadin()) {
            LOG.debug("Switching to Vaadin frame.");
            getDriver().switchTo().frame(findElementById("vaadin-content"));
        }
    }

    protected void selectDefaultFrame() {
        LOG.debug("Switching to default frame.");
        getDriver().switchTo().defaultContent();
    }

    public WebElement getVaadinPopup(final String title) {
        return getVaadinPopup(getDriver(), title);
    }

    private WebElement getVaadinPopup(final WebDriver driver, final String title) {
        selectVaadinFrame();
        try {
            LOG.debug("Checking for Vaadin popup '{}'", title);
            final By vaadinHeaderXpath = By.xpath("//div[@class='popupContent']//div[contains(text(), '" + title + "') and @class='v-window-header']");
            final WebElement el = getElementImmediately(vaadinHeaderXpath);
            LOG.debug("Found Vaadin popup '{}': {}", title, el.toString());
            return el;
        } catch (final Throwable t) {
            LOG.debug("Did not find Vaadin popup '{}'", title);
            return null;
        }
    }

    public void selectByVisibleText(final String id, final String text) {
        LOG.debug("selectByVisibleText: id={}, text={}", id, text);
        waitUntil(null, null, new Callable() {
            @Override public Boolean call() throws Exception {
                final Select select = getSelect(id);
                select.selectByVisibleText(text);
                return true;
            }
        });
    }

    /**
     * Vaadin usually wraps the select elements around a div element.
     * This method considers this.
     */
    public Select getSelect(final String id) {
        LOG.debug("Getting