org.vaadin.testbenchsauce.BaseTestBenchTestCase.java Source code

Java tutorial

Introduction

Here is the source code for org.vaadin.testbenchsauce.BaseTestBenchTestCase.java

Source

package org.vaadin.testbenchsauce;

import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.annotation.Nullable;
import javax.imageio.ImageIO;

import com.google.common.base.Predicate;
import com.google.gson.JsonObject;
import com.jayway.restassured.specification.RequestSpecification;
import com.rallydev.rest.RallyRestApi;
import com.rallydev.rest.request.CreateRequest;
import com.rallydev.rest.request.QueryRequest;
import com.rallydev.rest.response.CreateResponse;
import com.rallydev.rest.response.QueryResponse;
import com.rallydev.rest.util.Fetch;
import com.rallydev.rest.util.QueryFilter;
import com.vaadin.testbench.Parameters;
import com.vaadin.testbench.TestBench;
import com.vaadin.testbench.TestBenchTestCase;
import com.vaadin.testbench.screenshot.ImageFileUtil;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.Logger;
import org.apache.log4j.spi.LoggingEvent;
import org.fest.util.Strings;
import org.joda.time.Duration;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.openqa.selenium.By;
import org.openqa.selenium.Dimension;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.Keys;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.phantomjs.PhantomJSDriver;
import org.openqa.selenium.phantomjs.PhantomJSDriverService;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.support.ui.Select;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.testng.ITestNGMethod;
import org.testng.ITestResult;
import org.testng.Reporter;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.internal.BaseTestMethod;
import org.vaadin.testbenchsauce.webdriverwrapper.WebDriverWrapper;
import org.vaadin.testbenchsauce.webdriverwrapper.WebElementWrapper;

import static ch.lambdaj.Lambda.*;
import static com.jayway.restassured.RestAssured.given;
import static org.apache.commons.lang.StringUtils.trimToEmpty;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertFalse;
import static org.testng.AssertJUnit.assertNotNull;
import static org.testng.AssertJUnit.assertNull;
import static org.testng.AssertJUnit.assertTrue;
import static org.testng.AssertJUnit.fail;

abstract public class BaseTestBenchTestCase extends TestBenchTestCase {
    protected static final Logger logStep = Logger
            .getLogger(BaseTestBenchTestCase.class.getCanonicalName() + ".stepLogger");

    private static final boolean USE_REMOTE_WEB_DRIVER = true;
    private static final boolean TAKE_SCREENSHOT = false;
    public static final String USERNAME = "";
    public static final String PASSWORD = "";
    private static final String SESSION_COOKIE_NAME = "JSESSIONID";
    public static final int SLOW_FIND_ELEMENTS_WARNING_MILLIS = 150;

    public static int REMOTE_DRIVER_PORT;
    private static String BASE_URL;
    private static boolean USE_CHROME_DRIVER;
    private static boolean UPDATE_RALLY_TEST_CASE = false;
    private static String JENKINS_BUILD_NUMBER;
    private static String JENKINS_BUILD_URL;
    private static String RALLY_SERVER_URL;
    private static String RALLY_USERNAME;
    private static String RALLY_PASSWORD;

    static {
        String appDir = getPhantomJsPath();

        //Change to "true" to enable ChromeDriver (or launch with -dtestbench.useChromeDriver=true
        USE_CHROME_DRIVER = Boolean.parseBoolean(System.getProperty("testbench.useChromeDriver", "false"));

        BASE_URL = System.getProperty("testbench.baseUrl", "http://localhost:8080/quicktickets-dashboard-1b/");
        UPDATE_RALLY_TEST_CASE = Boolean
                .parseBoolean(System.getProperty("testbench.updateRallyTestCases", "false"));
        RALLY_SERVER_URL = System.getProperty("rallyServerUrl", "https://rally1.rallydev.com");
        RALLY_USERNAME = System.getProperty("rallyUsername", "not defined"); //
        RALLY_PASSWORD = System.getProperty("rallyPassword", "not defined");
        JENKINS_BUILD_NUMBER = System.getProperty("jenkinsBuildNumber", "unknown");
        JENKINS_BUILD_URL = System.getProperty("jenkinsBuildUrl",
                "javascript://alert('No jenkins url exists because this build was not run by jenkins for some reason...')");

        if (!USE_CHROME_DRIVER) {
            logStep.info("Initializing GhostDriver");
            launchPhantomJs(appDir);
        } else {
            logStep.info("Initializing ChromeDriver");
        }
    }

    private boolean closeBrowserOnFailEnabled = true;
    private WebDriver wrappedDriver;
    private Map<String, String> restAuthCookies;
    private TestngAppender testngAppender;
    private static int findElementCount;
    private static int tempScreenshotCounter;

    private static String getPhantomJsPath() {
        return System.getProperty("testbench.phantomjs.path",
                System.getProperty("user.dir") + "/src/test/resources/programs/phantomjs/");
    }

    public WebDriver getDriverWrapper() {
        if (wrappedDriver == null) {
            wrappedDriver = new WebDriverWrapper(super.getDriver());
        }
        return wrappedDriver;
    }

    private static void launchPhantomJs(String appDir) {
        //        System.out.println("Locating open port for phantomjs...");
        REMOTE_DRIVER_PORT = findFreePort();
        //        System.out.println("Found open port for phantomjs: "+ REMOTE_DRIVER_PORT);

        String executionString = appDir + "phantomjs.exe --proxy-type=none --webdriver-loglevel=info --webdriver="
                + REMOTE_DRIVER_PORT; //--webdriver-loglevel=debug for full js output and phantomjs commands 
        System.out.println("Executing " + executionString);
        CommandLine commandLine = CommandLine.parse(executionString);
        try {
            ProcessExecutor.runProcess(appDir, commandLine, new ProcessExecutorHandler() {
                @Override
                public void onStandardOutput(String msg) {
                    System.out.println("phantomjs:" + msg);
                }

                @Override
                public void onStandardError(String msg) {
                    System.err.println("phantomjs:" + msg);
                }
            }, 20 * 60 * 1000);
        } catch (IOException e) {
            throw new RuntimeException("Error starting phantomjs", e);
        }
    }

    private Integer walkThroughWaitMillis;

    @AfterMethod(alwaysRun = true)
    public void afterMethod(ITestResult testResult) {
        if (!testResult.isSuccess()) {
            onTestFailure(testResult);
        } else {
            assertNoWarningMessage();
        }

        if (UPDATE_RALLY_TEST_CASE) {
            updateTestDescriptionWithRallyLinks(testResult); //Let's Jenkins test reports link back to Rally
            updateRally(testResult);
        }

        quitDriver();
    }

    private void updateTestDescriptionWithRallyLinks(ITestResult testResult) {
        ITestNGMethod method = testResult.getMethod();
        String extraDescriptionDetail = null;
        if (method instanceof BaseTestMethod) {
            BaseTestMethod baseTestMethod = (BaseTestMethod) method;
            extraDescriptionDetail = "Rally test case(s): ";
            Method baseTestMethodMethod = baseTestMethod.getMethod();
            if (baseTestMethodMethod.isAnnotationPresent(RallyTestCase.class)) {
                String[] rallyTestCaseIds = baseTestMethodMethod.getAnnotation(RallyTestCase.class).value();
                if (rallyTestCaseIds != null) {
                    for (String rallyTestCaseId : rallyTestCaseIds) {
                        if (rallyTestCaseId != null && !rallyTestCaseId.isEmpty()) {
                            extraDescriptionDetail += "<a target='_blank' href=\"https://rally1.rallydev.com/#/search?keywords="
                                    + rallyTestCaseId + "\">" + rallyTestCaseId + "</a>&nbsp;";
                        }
                    }
                }

                if (extraDescriptionDetail != null) {
                    String description = method.getDescription();
                    baseTestMethod.setDescription(description != null ? description + " - " + extraDescriptionDetail
                            : extraDescriptionDetail);
                }
            }
        }
    }

    private void updateRally(ITestResult testResult) {
        //check to see if the test method has the RallyTestCase annoation.
        Method method = testResult.getMethod().getConstructorOrMethod().getMethod();
        if (method.isAnnotationPresent(RallyTestCase.class)) {
            String[] rallyTestCaseIds = method.getAnnotation(RallyTestCase.class).value();
            if (rallyTestCaseIds != null) {
                for (String rallyTestCaseId : rallyTestCaseIds) {
                    if (rallyTestCaseId != null && !rallyTestCaseId.isEmpty()) {
                        try {
                            updateRallyTestCase(rallyTestCaseId, testResult);
                        } catch (URISyntaxException e) {
                            printRallyServerError(e.getMessage());
                        } catch (IOException e) {
                            printRallyServerError(e.getMessage());
                        }
                    }
                }
            }
        }
    }

    private void printRallyServerError(String message) {
        System.out.println("------Rally Integration Error--------");
        System.out.println(message);
        System.out.println("Rally Server parameters:");
        System.out.println("rallyServerUrl:" + RALLY_SERVER_URL);
        System.out.println("rallyUsername:" + RALLY_USERNAME);
        System.out.println("rallyPassword:" + RALLY_PASSWORD);
        System.out.println("-------------------------------------");
    }

    private void updateRallyTestCase(String testCaseId, ITestResult testResult)
            throws URISyntaxException, IOException {
        //Rally API Setup
        RallyRestApi restApi = new RallyRestApi(new URI(RALLY_SERVER_URL), RALLY_USERNAME, RALLY_PASSWORD);
        restApi.setApplicationName("VaadinTestBench");

        int status = testResult.getStatus();
        String verdict = null;
        if (status == ITestResult.FAILURE) {
            verdict = "Fail";
        } else if (status == ITestResult.SUCCESS) {
            verdict = "Pass";
        } else {
            //For now if it's not a failure or success, don't update rally
            return;
        }

        Duration duration = new Duration(testResult.getStartMillis(), testResult.getEndMillis());

        try {
            //Get a reference to the test case
            QueryRequest testCaseRequest = new QueryRequest("TestCase");
            testCaseRequest.setFetch(new Fetch("FormattedID", "Name"));
            testCaseRequest.setQueryFilter(new QueryFilter("FormattedID", "=", testCaseId));
            QueryResponse testCaseResponse = restApi.query(testCaseRequest);
            if (testCaseResponse.getTotalResultCount() <= 0) {
                System.out.println("WARNING: Could not add test result to Rally test case '" + testCaseId
                        + "' because it was not found.");
                return;
            }

            JsonObject testCaseJsonObject = testCaseResponse.getResults().get(0).getAsJsonObject();
            String testCaseRef = testCaseJsonObject.get("_ref").getAsString();

            //Add a test result to that test case
            JsonObject testCaseResult = new JsonObject();
            testCaseResult.addProperty("Verdict", verdict);
            testCaseResult.addProperty("Build", JENKINS_BUILD_NUMBER);
            testCaseResult.addProperty("TestCase", testCaseRef);
            testCaseResult.addProperty("Date", new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").format(new Date()));
            testCaseResult.addProperty("Duration", duration.getMillis());

            //rallyTestCaseIntegration(SolutionsTabUiTest)
            String methodName = testResult.getName();
            Class realClass = testResult.getTestClass().getRealClass();

            String jobHref = "<b><a target='_blank' href=\"" + JENKINS_BUILD_URL + "\">Jenkins Job</a></b>";

            //testngreports/<package>/<package>.<Class>/<methodName>/
            String testUrl = JENKINS_BUILD_URL + "testngreports/" + realClass.getPackage().getName() + "/"
                    + realClass.getName() + "/" + methodName + "/";
            String testHref = "<a target='_blank' href=\"" + testUrl + "\">" + "Test Method: " + methodName + "("
                    + realClass.getSimpleName() + ")" + "</a>";
            String notes = jobHref + "&nbsp" + testHref;

            notes += "<div><b>Release Build Test Output:</b></div>";
            notes += getHtmlTestOutput(testResult);
            if (!testResult.isSuccess()) {
                notes += "<div style='color:red;'><b>Failure Details:</b></div>";

                notes += ExceptionUtils.getStackTrace(testResult.getThrowable());
            }

            testCaseResult.addProperty("Notes", notes);

            CreateRequest createTestResultRequest = new CreateRequest("testcaseresult", testCaseResult);
            CreateResponse createTestResultResponse = restApi.create(createTestResultRequest);

            //Output response
            if (createTestResultResponse.wasSuccessful()) {
                System.out.println("Created test result for Rally test case '" + testCaseId + "' - " + testCaseRef);
            } else {
                String[] createErrors;
                createErrors = createTestResultResponse.getErrors();
                System.out.println(
                        "WARNING: Error occurred creating Rally test case result for '" + testCaseId + "'");
                for (int i = 0; i < createErrors.length; i++) {
                    System.out.println(createErrors[i]);
                }
            }
        } finally {
            restApi.close();
        }
    }

    private String getHtmlTestOutput(ITestResult testResult) {
        StringBuilder output = new StringBuilder();
        for (Iterator<String> iterator = Reporter.getOutput(testResult).iterator(); iterator.hasNext();) {
            String line = iterator.next();
            output.append(line);
            if (iterator.hasNext()) {
                output.append("<br/>");
            }
        }
        return StringUtils.abbreviate(output.toString(), 4000); //don't overload rally, only allow up to 1000 characters
    }

    public void quitDriver() {
        if (isCloseBrowserOnFailEnabled() || !USE_CHROME_DRIVER) { //quit unless is closeBrowser disabled and using chrome driver
            getDriverWrapper().quit();
            wrappedDriver = null;
        }
    }

    /**
     * Preparing for actual test, create a firefox driver and define the address
     * where the app is located.
     */
    @BeforeMethod(alwaysRun = true)
    public final void setUpBase() throws Exception {
        //        System.out = new PrintStream();
        testngAppender = new TestngAppender();
        Logger.getRootLogger().addAppender(testngAppender);
        setUpInternal();
    }

    public void setUpInternal() throws Exception {
        initDriver();
        new Retry<Void>(new Retryable<Void>() {
            @Override
            public Void run() {
                if (USE_CHROME_DRIVER) {
                    driver.manage().window().setSize(new Dimension(1280, 1024));
                } else {
                    //look into if we can use driver.manage.window.setSize for phantomjs and not just chrome driver
                    testBench().resizeViewPortTo(1280, 1024);
                }

                return null;
            }
        }).run();
    }

    private void initDriver() throws Exception {
        System.setProperty("vaadin.testbench.developer.license", "license-here");
        Parameters.setScreenshotErrorDirectory("target/test-classes/screenshots/errors");
        if (USE_CHROME_DRIVER) {
            useChromeDriver();
        } else {
            useGhostDriver();
        }
        Parameters.setDebug(false);

        //init the driver wrapper
        getDriverWrapper();
    }

    private void onTestFailure(ITestResult testResult) {
        Reporter.setCurrentTestResult(testResult);

        onTestFailure(testResult.getName(), testResult.getTestClass().getName());

        Reporter.setCurrentTestResult(null);
    }

    public void onTestFailure(String methodName, String className) {
        if (getDriverWrapper() == null) {
            Reporter.log("Unable to take screenshot on failure, since getDriverWrapper is null");
            return;
        }
        // Grab a screenshot when a test fails
        try {
            BufferedImage screenshotImage = ImageIO.read(new ByteArrayInputStream(
                    ((TakesScreenshot) getDriverWrapper()).getScreenshotAs(OutputType.BYTES)));
            // Store the screenshot in the errors directory
            ImageFileUtil.createScreenshotDirectoriesIfNeeded();
            File errorScreenshotFile = ImageFileUtil
                    .getErrorScreenshotFile(methodName + "(" + className + ")" + ".png");
            System.out.println("Writing error image to: " + errorScreenshotFile.getCanonicalPath());
            ImageIO.write(screenshotImage, "png", errorScreenshotFile);
            //            Reporter.log("<a href='../../../" + errorScreenshotFile + "'>screenshot</a>"); //TODO: make work with real jenkins path.
        } catch (IOException e1) {
            e1.printStackTrace();
            Reporter.log("Couldn't create screenshot");
            Reporter.log(e1.getMessage());
        }
    }

    @AfterMethod(alwaysRun = true)
    public void tearDown() throws Exception {
        Logger.getRootLogger().removeAppender(testngAppender);
        if (TAKE_SCREENSHOT) {
            takeScreenshot();
        }
    }

    protected void takeScreenshot() {
        File screenshotAs = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
        String pathname = "c:/temp/screenshot_" + tempScreenshotCounter++ + ".png";
        try {
            FileUtils.copyFile(screenshotAs, new File(pathname));
        } catch (IOException e) {
            throw new RuntimeException("Unable to write screenshot file: " + pathname, e);
        }
    }

    private void useGhostDriver() throws Exception {
        final DesiredCapabilities desiredCapabilities = new DesiredCapabilities();
        desiredCapabilities.setCapability("takesScreenshot", true);
        final URL remoteAddress = new URL("http://localhost:" + REMOTE_DRIVER_PORT);
        if (USE_REMOTE_WEB_DRIVER) {
            RemoteWebDriver remoteWebDriver = new Retry<RemoteWebDriver>(new Retryable<RemoteWebDriver>() {
                @Override
                public RemoteWebDriver run() {
                    return new RemoteWebDriver(remoteAddress, desiredCapabilities);
                }
            }).run();
            setDriver(TestBench.createDriver(remoteWebDriver));
        } else {
            //Haven't tested this in a while, sicne the remote driver always gave higher performance
            desiredCapabilities.setCapability(PhantomJSDriverService.PHANTOMJS_EXECUTABLE_PATH_PROPERTY,
                    getPhantomJsPath() + "phantomjs.exe");
            PhantomJSDriver phantomJSDriver = new PhantomJSDriver(desiredCapabilities);
            //            phantomJSDriver.setLogLevel(java.util.logging.Level.ALL);
            driver = phantomJSDriver;
            setDriver(TestBench.createDriver(driver));
        }
    }

    private void useChromeDriver() {
        System.setProperty("webdriver.chrome.driver", System.getProperty("user.dir")
                + "/src/test/resources/programs/chromedriver/chromedriver2_win32_0.8/chromedriver.exe");
        setDriver(TestBench.createDriver(new ChromeDriver()));
    }

    protected boolean isCloseBrowserOnFailEnabled() {
        return closeBrowserOnFailEnabled;
    }

    public void keepBrowserOpenOnFail() {
        closeBrowserOnFailEnabled = false;
    }

    protected void assertSubWindowNotActive() {
        assertSubWindowActiveInternal(false);
    }

    protected void assertSubWindowActive() {
        assertSubWindowActiveInternal(true);
    }

    private void assertSubWindowActiveInternal(boolean expectActive) {
        boolean windowPresent = isElementPresent(By.className("v-window"));
        if (windowPresent && !expectActive) {
            fail("SubWindow was expect to be NOT be active, but was found");
        }
        if (!windowPresent && expectActive) {
            fail("SubWindow was expect to be be active, but was NOT found");
        }
    }

    protected void closeSubWindow() {
        getDriverWrapper().findElement(By.className("v-window-closebox")).click();
    }

    protected boolean isSelected(WebElement webElement) {
        String selected = webElement.getAttribute("selected");
        return "true".equals(selected) || "selected".equals(selected);
    }

    protected boolean isDisabled(WebElement webElement) {
        return webElement.getAttribute("class").contains("v-disabled");
    }

    protected void assertTabSelected(String title) {
        assertTabExists(title);
        WebElement element = findElement(com.vaadin.testbench.By
                .xpath("//div[contains(@class, 'v-tabsheet-tabitem-selected')]/div/div[contains(text(), '" + title
                        + "')]"));
        assertNotNull("Tab should have been selected containing text '" + title + "'", element);
    }

    protected void assertTextFieldHasFocus(String caption) {
        logStep.info("Asserting that field: " + caption + " has focus");
        WebElement current = getDriverWrapper().switchTo().activeElement();
        WebElement field = getTextField(caption);
        assertEquals("Expected field: " + caption + " to have focus", current, field);
    }

    protected void assertTextFieldHasValue(String textFieldCaption, String expectedValue) {
        logStep.info("Asserting that field: " + textFieldCaption + " has value: " + expectedValue);
        WebElement field = getTextField(textFieldCaption);
        assertEquals("Text field '" + textFieldCaption + "' has wrong value", expectedValue,
                getElementValue(field));
    }

    protected void assertFieldHasValue(WebElement textField, String expectedValue) {
        assertFieldHasValue(null, textField, expectedValue);
    }

    protected void assertFieldHasValue(String fieldDescription, WebElement textField, String expectedValue) {
        String formattedFieldDescription = fieldDescription == null ? "" : "'" + fieldDescription + "'";
        assertEquals("Field " + formattedFieldDescription + " value doesn't match", expectedValue,
                getElementValue(textField));
    }

    protected void assertTextFieldValuesMatch(WebElement email1, WebElement email2) {
        assertEquals("Field values don't match", getElementValue(email1), getElementValue(email2));
    }

    protected void assertTabExists(String title) {
        logStep.info("Asserting that tab exists: " + title);
        WebElement element = findElement(com.vaadin.testbench.By
                .xpath("//div[contains(@class, 'v-tabsheet-tabitem')]/div/div[contains(text(), '" + title + "')]"));
        assertNotNull("Tab not found: '" + title + "'. Found:" + getTabsAsText(), element);
    }

    protected void assertButtonExists(String title) {
        logStep.info("Asserting that button exists: " + title);
        WebElement button = getButton(title);
        assertNotNull("Button not found containing text '" + title + "'", button);
    }

    protected void assertButtonDoesNotExist(String title) {
        logStep.info("Asserting that button does not exist: " + title);
        WebElement button = getButton(title);
        assertNull("Button found containing text '" + title + "', expected none", button);
    }

    protected List<WebElement> getTableRows() {
        List<WebElement> elements = findElements(By.xpath("//tr[contains(@class, 'v-table-row')]"));
        assertNotNull("Could not find table rows", elements);
        return elements;
    }

    protected List<WebElement> getColumnsForTableRow(WebElement element) {
        return element.findElements(By.xpath(".//div[contains(@class,'v-table-cell-wrapper')]"));
    }

    protected WebElement getButton(String text) {
        return getButton(null, text);
    }

    protected WebElement getButton(WebElement parentElement, String text) {
        String xPath = "//span[contains(@class, 'v-button-caption') or contains(@class, 'v-nativebutton-caption')][contains(text(),'"
                + text + "')]";
        WebElement button;
        if (parentElement != null) {
            button = parentElement.findElement(By.xpath("." + xPath));
        } else {
            button = findElement(By.xpath(xPath));
        }
        return button;
    }

    protected void doPostOperationChecks() {
        assertNoExceptionOccurred();
        doWalkThroughWait();
        //enable to do get a screenshot history of the test
        //        takeScreenshot();
    }

    protected abstract void doPreOperationChecks();

    protected abstract void assertNoExceptionOccurred();

    protected void doWalkThroughWait() {
        if (walkThroughWaitMillis != null) {
            System.out.println("Walk-through Wait: " + walkThroughWaitMillis + "ms");
            sleep(walkThroughWaitMillis);
        }
    }

    protected void clickButton(String value, String failureMessage, WebElement parent) {
        doPreOperationChecks();
        logStep.info("Clicking button in a subsection of the page with value: " + value);
        WebElement button = getButton(parent, value);
        assertNotNull(failureMessage, button);
        button.click();
        doPostOperationChecks();
    }

    protected void clickButton(String value, String failureMessage) {
        doPreOperationChecks();
        logStep.info("Clicking button with value: " + value);
        WebElement button = getButton(value);
        assertNotNull(failureMessage, button);
        button.click();
        doPostOperationChecks();
    }

    protected void clickButton(String value) {
        clickButton(value, "Could not find button with label " + value + " to click");
    }

    protected void clickSaveButton() {
        clickButton("Save", "Save button was not found");
    }

    protected void clickApplyButton() {
        clickButton("Apply", "Apply button was not found");
    }

    protected void clickEditButton() {
        clickButton("Edit", "Edit button was not found");
    }

    protected void clickCancelButton(WebElement parent) {
        clickButton("Cancel", "Cancel button was not found", parent);
    }

    protected void clickCancelButton() {
        clickButton("Cancel", "Cancel button was not found");
    }

    protected void clickOkButton() {
        clickOkButton(null);
    }

    protected void clickOkButton(WebElement parent) {
        clickButton("Ok", "Ok button was not found", parent);
    }

    protected WebElement findElement(final By by) {
        return findElementInternal(by);
    }

    protected WebElement findElementInternal(final By by) {
        //        System.out.println("Find element count: " + findElementCount++);
        long startMillis = System.currentTimeMillis();
        List<WebElement> elements = getDriverWrapper().findElements(by); //findElements() is faster when expecting element not to be found
        WebElement element;
        if (elements.isEmpty()) {
            element = null;
        } else {
            element = elements.get(0);
        }
        long endMillis = System.currentTimeMillis();
        long duration = endMillis - startMillis;
        //        if (duration > SLOW_FIND_ELEMENTS_WARNING_MILLIS) {
        //            System.out.println("slow findElement() in " + duration + " ms for: " + by);
        //        }
        return element;

    }

    protected List<WebElement> findElements(final By by) {
        long startMillis = System.currentTimeMillis();
        List<WebElement> elements = getDriverWrapper().findElements(by);
        long endMillis = System.currentTimeMillis();
        long duration = endMillis - startMillis;
        //        if (duration > SLOW_FIND_ELEMENTS_WARNING_MILLIS) {
        //            System.out.println("slow findElements() in " + duration + " ms for: " + by);
        //        }
        return elements;
    }

    protected String getTabsAsText() {
        List<WebElement> elements = findElements(
                com.vaadin.testbench.By.xpath("//div[contains(@class, 'v-tabsheet-tabitem')]/div/div"));
        if (elements.isEmpty()) {
            return "No tabs found.";
        }
        return joinFrom(elements, ", ").getText();
    }

    protected void clickTab(String title) {
        clickTab(null, title);
    }

    protected void clickTab(WebElement element, String title) {
        doPreOperationChecks();
        logStep.info("Clicking tab with title: " + title);
        String xpathValue = "//div[contains(@class, 'v-tabsheet-tabitem')]/div/div[contains(text(), '" + title
                + "')]";
        WebElement tabDiv;
        if (element != null) {
            tabDiv = element.findElement(By.xpath("." + xpathValue));
        } else {
            tabDiv = findElement(By.xpath(xpathValue));
        }
        assertNotNull(tabDiv);
        tabDiv.click();
        assertTabSelected(title);
        doPostOperationChecks();
    }

    protected void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void openWithoutAutoLogin(String pageUrl) {
        open(pageUrl, false);
    }

    public void open(String pageUrl) {
        open(pageUrl, true);
    }

    private void open(String pageUrl, boolean performAutoLogin) {
        doPreOperationChecks();
        openInternal(pageUrl);
        if (performAutoLogin && loginIfNeededInternal()) {
            //doesn't jump to final url, so we need to try again.
            openInternal(pageUrl);
        }
        doPostOperationChecks();
    }

    /**
     * Not used anymore, seemed like Vaadin might be releasing itself to early after load, but now we just retry the open when that happens
     */
    private void blockUntilAppLoaded() {
        //v-app-loading
        WebDriverWait wait = new WebDriverWait(getDriverWrapper(), 10, 100);
        wait.until(new Predicate<WebDriver>() {
            @Override
            public boolean apply(@Nullable WebDriver webDriver) {
                WebElement appLoadingDiv = findElement(By.className("v-loading-indicator"));
                if (appLoadingDiv != null) {
                    System.out.println("v-loading-indicator found, displayed: " + appLoadingDiv.isDisplayed());
                } else {
                    System.out.println("****SOURCE:");
                    System.out.println(getDriverWrapper().getPageSource());
                }
                boolean loadingDivIsDisplayed = appLoadingDiv != null && appLoadingDiv.isDisplayed();
                if (loadingDivIsDisplayed) {
                    System.out.println(
                            "v-loading-indicator is displayed, shouldn't Base Testbench be catching this?!?");
                    //System.out.println(getDriverWrapper().getPageSource());
                }
                return !loadingDivIsDisplayed;
            }
        });
    }

    /**
     * Return true if no auto-redirect after login occurs and login was needed 
     */
    protected abstract boolean loginIfNeededInternal();

    private void openInternal(final String pageUrl) {
        //        WebElement appLoadingDiv = findElement(By.className("v-loading-indicator"));
        //In some odd cases, appLoadingDiv is shown (seems to be an underlying vaadin startup error occasionally

        final String url = getUrl(pageUrl);
        logStep.info("Opening page : " + url);
        getDriverWrapper().get(url);

        WebDriverWait wait = new WebDriverWait(getDriverWrapper(), 10, 500);
        wait.until(new Predicate<WebDriver>() {
            @Override
            public boolean apply(@Nullable WebDriver webDriver) {
                WebElement appLoadingDiv = findElement(By.className("v-loading-indicator"));
                boolean loadingDivIsDisplayed = appLoadingDiv != null && appLoadingDiv.isDisplayed();
                if (loadingDivIsDisplayed) {
                    logStep.error(
                            "v-loading-indicator is still displayed (TestBench would normally block until this is gone), retrying page load to avoid this intermittent Vaadin hiccup(turn driver logging on the see js exception");
                    getDriverWrapper().get(url);
                }
                return !loadingDivIsDisplayed;
            }
        });

        System.out.println("Page opened  : " + url);//not logStep since this doesn't seem like key step info.
    }

    private String getUrl(String pageUrl) {
        String url;
        if (pageUrl.startsWith("http")) {
            url = pageUrl;
        } else {
            url = BASE_URL + pageUrl;
        }
        return url;
    }

    protected void assertSuccessMessage() {
        logStep.info("Asserting success message is seen...");
        assertNoWarningMessage();
        final WebElement notification = getSuccessMessageElement();
        assertNotNull("The success message should have been displayed.", notification);
        new Retry<Boolean>(new Retryable<Boolean>() {
            @Override
            public Boolean run() {
                try {
                    return testBenchElement(((WebElementWrapper) notification).getWrappedElement())
                            .closeNotification(); //just clicking it causes it to fade away so next command fails
                } catch (StaleElementReferenceException ignored) {
                    //2nd chance
                    WebElement notificationSecondChance = getSuccessMessageElement();
                    if (notificationSecondChance != null) {
                        notificationSecondChance.click();
                        sleep(200); //to allow fade away to occur
                    }
                }
                return false;
            }
        }).run();

    }

    protected void assertNoWarningMessage() {
        assertNull("A warning message should not have been displayed.", getWarningMessageElement());
    }

    private WebElement getSuccessMessageElement() {
        return findElement(By.xpath("//div[contains(@class,'success-notification')]"));
    }

    protected String getElementValue(WebElement element) {
        return element.getAttribute("value");
    }

    protected void assertElementValueMatches(WebElement element, String expectedValue) {
        assertEquals(expectedValue, getElementValue(element));
    }

    protected void assertWarningMessage() {
        logStep.info("Asserting warning message is seen...");
        assertNull("A success message should not have been displayed.", getSuccessMessageElement());
        final WebElement notification = getWarningMessageElement();
        assertNotNull("A warning message should have been displayed.", notification);
        new Retry<Boolean>(new Retryable<Boolean>() {
            @Override
            public Boolean run() {
                try {
                    return testBenchElement(((WebElementWrapper) notification).getWrappedElement())
                            .closeNotification(); //just clicking it causes it to fade away so next command fails
                } catch (StaleElementReferenceException ignored) {
                    //2nd chance
                    WebElement notificationSecondChance = getWarningMessageElement();
                    if (notificationSecondChance != null) {
                        notificationSecondChance.click();
                        sleep(200); //to allow fade away to occur
                    }
                }
                return false;
            }
        }).run();
    }

    private WebElement getWarningMessageElement() {
        return findElement(By.xpath("//div[contains(@class,'warning-notification')]"));
    }

    protected Select getSelect(String label) {
        //first attempt to find the select within a form layout
        WebElement element = findElement(By.xpath("//div[contains(@class, 'v-caption')]/span[contains(text(),'"
                + label + "')]/../../../td[contains(@class, 'v-formlayout-contentcell')]/div/select"));

        if (element == null) {
            //select appears to be outside a form
            element = findElement(By.xpath(
                    "//span[contains(@class,'v-caption') and contains(text(),'" + label + "')]/../../div/select"));
        }

        assertNotNull("Select was not found with label:" + label, element);
        return new Select(element);
    }

    protected void setSelectValue(String caption, int index) {
        doPreOperationChecks();
        logStep.info("Setting select with caption: " + caption + " to index: " + index);
        Select select = getSelect(caption);
        select.selectByIndex(index);
        doPostOperationChecks();
    }

    protected WebElement setTextInTextField(String caption, String value) {
        logStep.info("Setting text in text field with caption '" + caption + "' to '" + value + "'");
        WebElement textField = getTextField(caption);
        assertNotNull("Could not set text in textfield with caption " + caption + " because it wasn't found.",
                textField);
        return setTextInTextField(caption, textField, value);
    }

    private WebElement setTextInTextField(String fieldDescription, WebElement textField, String value) {
        doPreOperationChecks();
        assertNotNull("Could not set text because field was wasn't found.", textField);
        textField.click(); //so that it receives focus, otherwise sendKeys() loses characters
        textField.clear();

        textField.sendKeys(value);
        assertFieldHasValue(fieldDescription, textField, value);

        unblurField(textField);
        doPostOperationChecks();
        return textField;
    }

    protected void unblurField(WebElement textField) {
        //Send TAB key - only works in Ghost driver (doesn't work in chrome, etc.)
        textField.sendKeys(Keys.TAB); //Otherwise when a "per character" text listener is used, the results of it will appear too late for hte next test assert. //Is this really working??? Only in Ghost driver.

        if (USE_CHROME_DRIVER) {
            //Also do a click on the root, works as unblur for chrome, etc but not ghost driver.
            System.out.println("TODO: find an innocuous area to click to force unblur");
            //findElement(By.id("logoContainer")).click(); //create a blur by click on a known innocuous part of the page
        }
    }

    protected void setTextInTextFieldById(String textFieldId, String value) {
        logStep.info("Setting text in text field with id: " + textFieldId + " to: " + value);
        WebElement textField = getElementById(textFieldId);
        assertNotNull("Could not set text in textfield with id " + textFieldId + " because it wasn't found.",
                textField);
        textField.click(); //so that it receives focus, otherwise sendKeys() loses characters
        textField.clear();
        textField.sendKeys(value);
        assertFieldHasValue("id: " + textFieldId, textField, value);
        unblurField(textField);
    }

    protected WebElement setTextInTextArea(String caption, String value) {
        logStep.info("Setting text in text area with caption: " + caption + " to: " + value);
        WebElement textArea = getTextArea(caption);
        assertNotNull("Could not set text in text area with caption " + caption + " because it wasn't found.",
                textArea);
        return setTextInTextField(caption, textArea, value);
    }

    protected WebElement setLocalDateTimeInDateField(String caption, LocalDateTime localDateTime) {
        doPreOperationChecks();
        String localDateAsString = localDateTime == null ? "" : localDateTime.toString("MM/dd/yyyy hh:mm aaa");
        logStep.info("Setting date in date field with caption: " + caption + " to: " + localDateAsString);
        WebElement textField = getDateField(caption);
        assertNotNull("Could not set date in datefield with caption " + caption + " because it wasn't found.",
                textField);
        textField.click(); //so that it receives focus, otherwise sendKeys() loses characteres
        textField.clear();
        textField.sendKeys(localDateAsString);
        assertNoExceptionOccurred();
        assertFieldHasValue(caption, textField, localDateAsString);
        unblurField(textField);
        doPostOperationChecks();
        return textField;

    }

    protected WebElement setLocalDateInDateField(String caption, LocalDate localDate) {
        String localDateAsString = localDate == null ? "" : localDate.toString("MM/dd/yyyy");
        return setLocalDateInDateFieldAsString(caption, localDateAsString);
    }

    protected WebElement setLocalDateInDateFieldAsString(String caption, String dateString) {
        doPreOperationChecks();
        logStep.info("Setting date in date field with caption: " + caption + " to: " + dateString);
        WebElement textField = getDateField(caption);
        assertNotNull("Could not set date in datefield with caption " + caption + " because it wasn't found.",
                textField);
        textField.click(); //so that it receives focus, otherwise sendKeys() loses characters
        textField.clear();
        textField.sendKeys(dateString);
        assertNoExceptionOccurred();
        assertFieldHasValue(caption, textField, dateString);
        unblurField(textField);
        doPostOperationChecks();
        return textField;
    }

    protected void assertFieldIsReadOnly(String caption) {
        doPreOperationChecks();
        WebElement field = getField(caption);
        assertNotNull("Could not assert field is read-only with caption " + caption + " because it wasn't found.",
                field);
        assertThat(field.getAttribute("class"))
                .describedAs("Field with caption " + caption + " should have been read-only")
                .contains("v-readonly");
        doPostOperationChecks();
    }

    protected WebElement getTextField(String caption) {
        return getField(caption);
    }

    protected WebElement getDateField(String caption) {
        return getField(caption);
    }

    private WebElement getField(String caption) {
        return findElement(By.xpath(
                "//div[contains(@class,'v-caption')]/span[contains(text(),'" + caption + "')]/../../..//input"));
    }

    protected WebElement getTextArea(String caption) {
        return findElement(By.xpath(
                "//div[contains(@class,'v-caption')]/span[contains(text(),'" + caption + "')]/../../..//textarea"));
    }

    protected WebElement getLabel(String text) {
        return findElement(By.xpath(
                "//*[self::div or self::span][contains(@class,'v-label')][contains(text(),'" + text + "')]"));
    }

    protected void assertLabelExists(String text) {
        logStep.info("Asserting that label exists: '" + text + "'");
        WebElement label = getLabel(text);
        assertNotNull("Could not find label with text: " + text, label);
        assertTrue("Expected label found, but is not displayed: " + text, label.isDisplayed());
    }

    protected void assertLabelNotExists(String text) {
        logStep.info("Asserting that label does not exist: '" + text + "'");
        WebElement label = getLabel(text);
        if (label != null) {
            assertFalse("Unexpected label found, but and is displayed: " + text, label.isDisplayed());
        }
    }

    protected void assertDateFieldHasValue(String textFieldCaption, String expectedValue) {
        logStep.info("Asserting that date field '" + textFieldCaption + "' has value: " + expectedValue);
        WebElement field = getDateField(textFieldCaption);
        assertEquals("Date field '" + textFieldCaption + "' has wrong value", expectedValue,
                getElementValue(field));
    }

    protected void clickCheckBox(String caption) {
        doPreOperationChecks();
        WebElement checkBox = getCheckBox(caption);
        assertNotNull("Could not click on checkbox with caption " + caption + " because it wasn't found.");
        checkBox.click();
        doPostOperationChecks();
    }

    private WebElement getCheckBox(String caption) {
        return findElement(By.xpath(
                "//span[contains(@class, 'v-checkbox')]/label[contains(text(), '" + caption + "')]/../input"));
    }

    /**
     * Find table cell with a particular cell value. Note this method will only look for visible cells currently displayed in the table.
     */
    protected void assertCellInTable(String tableCaption, String cellValue) {
        if (Strings.isNullOrEmpty(tableCaption)) {
            logStep.info("Asserting that cell in first table on page has value:" + cellValue);

        } else {
            logStep.info("Asserting that cell in table with caption: " + tableCaption + " has value:" + cellValue);
        }
        WebElement table = getTableByCaption(tableCaption);
        assertNotNull("Could not find table with caption:" + table);
        WebElement cell = getCellInTable(table, cellValue);
        assertNotNull("Could not find cell in table with value:" + cellValue, cell);
    }

    //Highly custom - Only working if you put the count in the caption using the string "Total Records" 
    //    protected int getTableRowCount() {
    //        WebElement element = findElement(By.xpath("//span[contains(@class, 'v-captiontext') and contains(text(),'Total Records')]"));
    //        if (element == null) {
    //            //try non-vaadin-caption way of placing "Row Count" on the page
    //            element = findElement(By.xpath("//div[contains(@class, 'v-label') and contains(text(),'Total Records')]"));
    //        }
    //        assertNotNull("Could not find table row count label 'Total Records'",element);
    //        String count = element.getText().split(":")[1].trim();
    //        return Integer.parseInt(count);
    //    }

    private WebElement getCellInTable(WebElement table, String cellValue) {
        try {
            return table.findElement(By.xpath(".//div[contains(text(), '" + cellValue + "')]"));//Note the .// that will start the search at the table element passed in.
        } catch (Exception e) {
            return null;
        }
    }

    private WebElement getTableByCaption(String caption) {
        if (Strings.isNullOrEmpty(caption)) {
            return findElement(By.xpath("//div[contains(@class, 'v-table')]"));
        }
        return findElement(By.xpath("//span[contains(@class, 'v-captiontext') and contains(text(), '" + caption
                + "')]/../../div[contains(@class, 'v-table')]"));
    }

    protected void setComboboxValue(String caption, String value) {
        setComboboxValue(null, caption, value);
    }

    protected void setComboboxValue(WebElement element, String caption, String value) {
        doPreOperationChecks();
        WebElement input = null;
        String xpathValue = "//div[contains(@class, 'v-caption')]/span[contains(text(),'" + caption
                + "')]/ancestor::tr[contains(@class,'v-formlayout-row')][1]//input";
        if (element == null) {
            //global search
            input = findElement(By.xpath(xpathValue));
        } else {
            //search within element that was passed in
            input = element.findElement(By.xpath("." + xpathValue));
        }
        assertNotNull("Could not find combobox with caption '" + caption + "'", input);
        input.click();
        input.sendKeys(value);
        input.sendKeys(Keys.TAB);
        assertFieldHasValue(caption, input, value);
        doPostOperationChecks();
    }

    protected WebElement getElementById(String elementId) {
        return findElement(By.id(elementId));
    }

    protected void assertFieldIsBlankById(String fieldId) {
        System.out.println("Checking to make sure " + fieldId + " is blank");
        WebElement element = getElementById(fieldId);
        assertNotNull("Could not find field with id " + fieldId, element);
        assertElementIsBlank(element);
    }

    protected void assertElementIsBlank(WebElement field) {
        assertEquals("Field should be blank", "", trimToEmpty(getElementValue(field)));
    }

    public void setWalkThroughWaitMillis(Integer walkThroughWaitMillis) {
        this.walkThroughWaitMillis = walkThroughWaitMillis;
    }

    private static int findFreePort() {
        ServerSocket socket = null;
        try {
            socket = new ServerSocket(0);
            socket.setReuseAddress(true);
            int port = socket.getLocalPort();
            try {
                socket.close();
            } catch (IOException e) {
                // Ignore IOException on close()
            }
            return port;
        } catch (IOException ignored) {
        } finally {
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException ignored) {
                }
            }
        }
        throw new IllegalStateException("Could not find a free TCP/IP port to start embedded Jetty HTTP Server on");
    }

    protected void clickBrowserBack() {
        doPreOperationChecks();
        logStep.info("Clicking browser back button");
        getDriverWrapper().navigate().back();
        logStep.info("After back, at: " + getDriverWrapper().getCurrentUrl());
        doPostOperationChecks();
    }

    protected void clickBrowserForward() {
        doPreOperationChecks();
        logStep.info("Clicking browser forward button");
        logStep.info("After forward, at: " + getDriverWrapper().getCurrentUrl());
        getDriverWrapper().navigate().forward();
        doPostOperationChecks();
    }

    protected void assertViewHashExists(String viewName) {
        String currentUrl = getDriverWrapper().getCurrentUrl();
        assertTrue(currentUrl.contains("!" + viewName));
    }

    protected void assertRadioButtonSelected(String formFieldCaption, String radioButtonCaption) {
        logStep.info("Asserting radio button is selected: with form caption " + formFieldCaption
                + " and radio button caption " + radioButtonCaption);
        WebElement radioButton = getRadioButton(formFieldCaption, radioButtonCaption);
        assertNotNull("Could not find radio button with form caption " + formFieldCaption
                + " and radio button caption " + radioButtonCaption, radioButton);
        assertThat(radioButton.isSelected());
    }

    protected void clickRadioButton(String formFieldCaption, String radioButtonCaption) {
        doPreOperationChecks();
        logStep.info("Clicking: radio button with form caption " + formFieldCaption + " and radio button caption "
                + radioButtonCaption);
        WebElement radioButton = getRadioButton(formFieldCaption, radioButtonCaption);
        assertNotNull("Could not click on checkbox with form caption " + formFieldCaption
                + " and radio button caption " + radioButtonCaption + " because it was not found", radioButton);
        radioButton.click();
        doPostOperationChecks();
    }

    private WebElement getRadioButton(String formFieldCaption, String radioButtonCaption) {
        return findElement(By.xpath("//div[contains(@class,'v-caption')]/span[contains(text(),'" + formFieldCaption
                + "')]/../../../td[contains(@class,'v-formlayout-contentcell')]/div/span/label[contains(text(),'"
                + radioButtonCaption + "')]/../input"));
    }

    protected void loginRestClient() {
        String loginUrl = "TODO-app-specific";
        restAuthCookies = given().param("username", USERNAME).param("password", PASSWORD).expect().statusCode(200)
                .post(getRestUrl() + loginUrl).getCookies();
    }

    private String getRestUrl() {
        return "TODO:put any rest url here";
    }

    protected RequestSpecification givenWithAuth(String description) {
        if (description != null) {
            logStep.info("REST call ): " + description);
        }
        if (restAuthCookies == null) {
            loginRestClient();
        }
        return given().cookie(SESSION_COOKIE_NAME, restAuthCookies.get(SESSION_COOKIE_NAME));
    }

    protected void refreshPage() {
        String currentUrl = getDriverWrapper().getCurrentUrl();
        logStep.info("Refreshing the page (first going to blank to avoid picking up old data: " + currentUrl);
        open("about:blank"); //to insure we are cleared, otherwise the wait hooks don't seem to work
        open(currentUrl);
    }

    public Object executeScript(String script, Object... args) {
        doPreOperationChecks();
        System.out.println("  Executing: " + script);
        Object object = ((JavascriptExecutor) getDriverWrapper()).executeScript(script, args);
        doPostOperationChecks();
        return object;
    }

    public Object executeScriptAsync(String script, Object... args) {
        doPreOperationChecks();
        System.out.println("  Executing(async): " + script);
        Object object = ((JavascriptExecutor) getDriverWrapper()).executeAsyncScript(script, args);
        doPostOperationChecks();
        return object;
    }

    public static final String EXTERNAL_OPERATION_IN_PROGRESS_JS_VARIABLE = "window.fooo_vaadin_externalOperationInProgress";

    protected void performSomeJsOperation(int propertyId) {
        logStep.info("performSomeJsOperation" + propertyId);

        executeScript(EXTERNAL_OPERATION_IN_PROGRESS_JS_VARIABLE + " = true"); //to allow testing tools to determine when the non-vaadin operation is complete. The work context change will trigger a vaadin call, which when complete will set the flag to false.
        executeScript("foooo_barr.someJsOperation(" + propertyId + ")");
        waitForExternalOperationCompleteFlag();
    }

    protected void waitForExternalOperationCompleteFlag() {
        WebDriverWait wait = new WebDriverWait(getDriverWrapper(), 10);
        wait.until(new Predicate<WebDriver>() {
            @Override
            public boolean apply(@Nullable WebDriver webDriver) {
                Object inProgress = executeScript("return " + EXTERNAL_OPERATION_IN_PROGRESS_JS_VARIABLE);
                testBench().waitForVaadin(); //not sure if this matters, but is at least quick! :)
                return Boolean.FALSE.equals(inProgress);
            }
        });
    }

    protected void assertGridCellTextFieldEnabledInRow(String rowTitle, int rowIndexAfterTitle,
            boolean expectedEnabled, WebElement webElement) {
        logStep.info("Asserting that input in grid with row title '" + rowTitle + "' and index(after title): "
                + rowIndexAfterTitle + " is enabled: " + expectedEnabled);
        WebElement field = getGridCellTextFieldInRow(rowTitle, rowIndexAfterTitle, webElement);
        assertNotNull("Could not find input in grid with row title '" + rowTitle + "' and index(after title): "
                + rowIndexAfterTitle, field);
        if (expectedEnabled && isDisabled(field)) {
            fail("Text field in grid with row title '" + rowTitle + "' and index(after title): "
                    + rowIndexAfterTitle + " has wrong enabled state, expected enable state: " + expectedEnabled);
        }
        if (!expectedEnabled & !isDisabled(field)) {
            fail("Text field in grid with row title '" + rowTitle + "' and index(after title): "
                    + rowIndexAfterTitle + " has wrong enabled state, expected enable state: " + expectedEnabled);
        }
    }

    protected void assertGridCellTextFieldHasValueInRow(String rowTitle, int rowIndexAfterTitle,
            String expectedValue, WebElement webElement) {
        logStep.info("Asserting that input in grid with row title '" + rowTitle + "' and index(after title): "
                + rowIndexAfterTitle + " has value: "
                + (Strings.isNullOrEmpty(expectedValue) ? "-Unset-" : expectedValue));
        WebElement field = getGridCellTextFieldInRow(rowTitle, rowIndexAfterTitle, webElement);
        assertNotNull("Could not find input in grid with row title '" + rowTitle + "' and index(after title): "
                + rowIndexAfterTitle, field);
        assertEquals("Text field in grid with row title '" + rowTitle + "' and index(after title): "
                + rowIndexAfterTitle + "' has wrong value", expectedValue, getElementValue(field));
    }

    protected void assertGridCellLabelHasValueInRow(String rowTitle, int rowIndexAfterTitle, String expectedValue,
            WebElement webElement) {
        logStep.info("Asserting that label in grid with row title '" + rowTitle + "' and index(after title): "
                + rowIndexAfterTitle + " has value: " + expectedValue);
        WebElement gridCellElement = getGridCellInRow(rowTitle, rowIndexAfterTitle, webElement);
        assertNotNull("Could not find cell in grid with row title '" + rowTitle + "' and index(after title): "
                + rowIndexAfterTitle, gridCellElement);
        WebElement label = gridCellElement.findElement(By.xpath(".//div[contains(@class, 'v-label')"));
        assertNotNull("Could not find label in grid with row title '" + rowTitle + "' and index(after title): "
                + rowIndexAfterTitle, label);
        assertEquals("Label in grid with row title '" + rowTitle + "' and index(after title): " + rowIndexAfterTitle
                + "' has wrong value", expectedValue, label.getText());
    }

    protected void setGridCellTextValueInRow(String rowTitle, int rowIndexAfterTitle, String value,
            WebElement baseElement) {
        WebElement field = getGridCellTextFieldInRow(rowTitle, rowIndexAfterTitle, baseElement);
        assertNotNull("Could not find input in grid with row title '" + rowTitle + "' and index(after title): "
                + rowIndexAfterTitle, field);
        setTextInTextField("text field in grid with row title '" + rowTitle + "' and index(after title): "
                + rowIndexAfterTitle, field, value);
    }

    private WebElement getGridCellTextFieldInRow(String rowTitle, int rowIndexAfterTitle, WebElement baseElement) {
        WebElement gridCellElement = getGridCellInRow(rowTitle, rowIndexAfterTitle, baseElement);
        assertNotNull("Could not find cell in grid with row title '" + rowTitle + "' and index(after title): "
                + rowIndexAfterTitle, gridCellElement);
        WebElement input = gridCellElement.findElement(By.tagName("input"));
        assertNotNull("Could not find input in grid with row title '" + rowTitle + "' and index(after title): "
                + rowIndexAfterTitle, input);
        return input;
    }

    private WebElement getGridCellInRow(String rowTitle, int rowIndexAfterTitle, WebElement baseElement) {
        return baseElement.findElement(By.xpath(
                ".//div[contains(@class, 'v-gridlayout')]/descendant::div[contains(@class, 'v-label') and contains(text(), '"
                        + rowTitle + "')]"
                        + "/ancestor::div[contains(@class, 'v-gridlayout-slot')][1]/following-sibling::div["
                        + rowIndexAfterTitle + "]"));
    }

    /**
     * Not yet workign reliably in both ChromeDriver and GhostDriver
     */
    protected void assertTooltipAppears(String fieldCaption, String tooltipText) {
        logStep.info(
                "Asserting that tooltip appears on field '" + fieldCaption + "' with tooltip text: " + tooltipText);
        WebElement textField = getTextField(fieldCaption);

        testBenchElement(((WebElementWrapper) textField).getWrappedElement()).showTooltip();
        //        System.out.println("showTooltip: " + findElement(By.xpath("//div[contains(@class,'v-tooltip')]")));
        //        
        //        new Actions(getDriverWrapper()).moveToElement(textField, 3, 3).build().perform();
        //        System.out.println("moveToElement: " + findElement(By.xpath("//div[contains(@class,'v-tooltip')]")));

        assertNotNull(
                "Tooltip not found (note: make sure to click save button or showing tooltip fails on ghost driver) with text: "
                        + tooltipText,
                findElement(By.xpath(
                        "//div[contains(@class,'v-tooltip')]//div[contains(text(),'" + tooltipText + "')]")));
    }

    protected void assertDateFieldValidation(String fieldCaption, LocalDate value, String validationText) {
        assertDateFieldValidation(fieldCaption, value, validationText, false);
    }

    protected void assertDateFieldValidation(String fieldCaption, LocalDate value, String validationText,
            boolean isApplyButton) {
        logStep.info("Field Validation: Asserting date field '" + fieldCaption + "' set to '" + value
                + "' shows validation message: " + validationText);
        setLocalDateInDateField(fieldCaption, value);
        assertValidationText(fieldCaption, validationText, isApplyButton);
    }

    protected void assertFieldValidation(String fieldCaption, String value, String validationText) {
        assertFieldValidation(fieldCaption, value, validationText, false);
    }

    protected void assertFieldValidation(String fieldCaption, String value, String validationText,
            boolean isApplyButton) {
        logStep.info("Field Validation: Asserting field '" + fieldCaption + "' set to '" + value
                + "' shows validation message: " + validationText);
        setTextInTextField(fieldCaption, value);
        assertValidationText(fieldCaption, validationText, isApplyButton);
    }

    private void assertValidationText(String fieldCaption, String validationText, boolean isApplyButton) {
        if (isApplyButton) {
            clickApplyButton();
        } else {
            clickSaveButton();
        }
        logStep.info("TODO: Using simplified approach, reassert tooltip when can be done reliably");
        assertWarningMessage();
        //        assertTooltipAppears(fieldCaption, validationText);
    }

    public void clickContextMenuItem(String contentMenuItemToFind) {
        WebElement element = findElement(By.xpath("//span[contains(@class,'v-button-caption') and contains(text(),'"
                + contentMenuItemToFind + "')]"));
        assertNotNull("Could not find the menu item '" + contentMenuItemToFind
                + "' in the dashboard content selection menu.", element);
        element.click();
    }

    protected void clickPieChartLabel(String name) {
        logStep.info("Looking for chart label: " + name);
        WebElement element = findElement(By.xpath("//*[name()='tspan'][contains(text(),'" + name + "')]"));
        assertNotNull("Label '" + name + "' could not be found ", element);
        element.click();
    }

    protected void assertTableIsEmpty(String tableCaption) {
        assertCellInTable(tableCaption, "No data found");
    }

    //    protected void assertTableRowCount(int expectedCount) {
    //        logStep.info("Asserting that first table found has row count: " + expectedCount);
    //        assertEquals(getTableRowCount(), expectedCount);
    //    }

    protected void selectMultiSelectOptionByIndex(String caption, int listSelectIndex) {
        logStep.info("Setting multi-select with caption: " + caption + " to index: " + listSelectIndex);
        //Chrome driver and ghost driver don't support clicking multi selects well (on change isn't fired)- bug reports - due to https://code.google.com/p/chromedriver/issues/detail?id=496&q=select&colspec=ID%20Status%20Pri%20Owner%20Summary and https://code.google.com/p/chromedriver/issues/detail?id=169#c9
        //Workaround is to arrow down to the item (which does trigger the on change logic)
        WebElement selectElement = getSelectElement(caption);

        assertNotNull("Could not find the multi-select field '" + caption + "'.", selectElement);

        selectElement.sendKeys(Keys.HOME);
        for (int i = 0; i < listSelectIndex; i++) {
            selectElement.sendKeys(Keys.ARROW_DOWN);
        }
    }

    protected WebElement getSelectElement(String optionGroupLabel) {
        return findElement(By.xpath("//span[contains(text(),'" + optionGroupLabel
                + "')]/../../..//select[contains(@class, 'v-select-select')]"));
    }

    protected WebElement getDivOrSpanWithClassAndText(String className, String text) {
        return findElement(By.xpath("//*[self::div or self::span][contains(@class,'" + className
                + "')][contains(text()," + escapeQuotes(text) + ")]"));
    }

    protected String selectComboBoxOptionByIndex(int optionIndex) {
        logStep.info("Selecting combobox(0) option index: " + optionIndex);

        WebElement comboBox = findElement(By.xpath("//div[contains(@class, 'v-filterselect')]"));
        WebElement filterSelectButton = comboBox.findElement(By.className("v-filterselect-button"));//show how to find this in the chrome dev tools
        filterSelectButton.click();
        WebElement filterInput = comboBox.findElement(By.className("v-filterselect-input"));
        filterInput.sendKeys(Keys.ARROW_DOWN); //one to enter list selection
        for (int i = 0; i < optionIndex; i++) {
            filterInput.sendKeys(Keys.ARROW_DOWN);
        }
        filterInput.sendKeys(Keys.ENTER);
        return getElementValue(filterInput);
    }

    private static class TestngAppender extends AppenderSkeleton {
        @Override
        protected void append(LoggingEvent loggingEvent) {
            Reporter.log(String.valueOf(loggingEvent.getMessage()));
        }

        @Override
        public void close() {

        }

        @Override
        public boolean requiresLayout() {
            return false;
        }
    }

    @SuppressWarnings("IndexOfReplaceableByContains")
    protected String escapeQuotes(String toEscape) {
        // Convert strings with both quotes and ticks into: foo'"bar -> concat("foo'", '"', "bar")
        if (toEscape.indexOf("\"") > -1 && toEscape.indexOf("'") > -1) {
            boolean quoteIsLast = false;
            if (toEscape.lastIndexOf("\"") == toEscape.length() - 1) {
                quoteIsLast = true;
            }
            String[] substrings = toEscape.split("\"");

            StringBuilder quoted = new StringBuilder("concat(");
            for (int i = 0; i < substrings.length; i++) {
                quoted.append("\"").append(substrings[i]).append("\"");
                quoted.append(((i == substrings.length - 1) ? (quoteIsLast ? ", '\"')" : ")") : ", '\"', "));
            }
            return quoted.toString();
        }

        // Escape string with just a quote into being single quoted: f"oo -> 'f"oo'
        if (toEscape.indexOf("\"") > -1) {
            return String.format("'%s'", toEscape);
        }

        // Otherwise return the quoted string
        return String.format("\"%s\"", toEscape);
    }
}