/* * Copyright 2000-2014 Vaadin Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * 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 com.vaadin.tests.tb3; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.net.URL; import java.util.Collections; import java.util.List; import com.vaadin.testbench.TestBenchElement; import org.junit.After; import org.junit.Before; import org.junit.runner.RunWith; import org.openqa.selenium.*; import org.openqa.selenium.interactions.HasInputDevices; import org.openqa.selenium.interactions.Keyboard; import org.openqa.selenium.interactions.Mouse; import org.openqa.selenium.interactions.internal.Coordinates; import org.openqa.selenium.internal.Locatable; import org.openqa.selenium.remote.BrowserType; import org.openqa.selenium.remote.DesiredCapabilities; import org.openqa.selenium.remote.RemoteWebDriver; import org.openqa.selenium.support.ui.ExpectedCondition; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; import com.thoughtworks.selenium.webdriven.WebDriverBackedSelenium; import com.vaadin.server.LegacyApplication; import com.vaadin.server.UIProvider; import com.vaadin.testbench.TestBench; import com.vaadin.testbench.TestBenchTestCase; import com.vaadin.tests.components.AbstractTestUIWithLog; import com.vaadin.tests.tb3.MultiBrowserTest.Browser; import com.vaadin.ui.UI; import static com.vaadin.tests.tb3.TB3Runner.localWebDriverIsUsed; /** * Base class for TestBench 3+ tests. All TB3+ tests in the project should * extend this class. * * Provides: * * * @author Vaadin Ltd */ @RunWith(value = TB3Runner.class) public abstract class AbstractTB3Test extends TestBenchTestCase { /** * Height of the screenshots we want to capture */ private static final int SCREENSHOT_HEIGHT = 850; /** * Width of the screenshots we want to capture */ private static final int SCREENSHOT_WIDTH = 1500; /** * Timeout used by the TB grid */ private static final int BROWSER_TIMEOUT_IN_MS = 30 * 1000; private DesiredCapabilities desiredCapabilities; private boolean debug = false; private boolean push = false; { // Default browser to run on unless setDesiredCapabilities is called desiredCapabilities = Browser.FIREFOX.getDesiredCapabilities(); } /** * Connect to the hub using a remote web driver, set the canvas size and * opens the initial URL as specified by {@link #getTestUrl()} * * @throws Exception */ @Before public void setup() throws Exception { setupDriver(); } /** * Creates and configure the web driver to be used for the test. By default * creates a remote web driver which connects to {@link #getHubURL()} and * selects a browser based on {@link #getDesiredCapabilities()}. * * This method MUST call {@link #setDriver(WebDriver)} with the newly * generated driver. * * @throws Exception * If something goes wrong */ protected void setupDriver() throws Exception { DesiredCapabilities capabilities; Browser runLocallyBrowser = getRunLocallyBrowser(); if (runLocallyBrowser != null) { if (System.getenv().containsKey("TEAMCITY_VERSION")) { throw new RuntimeException( "@RunLocally is not supported for tests run on the build server"); } capabilities = runLocallyBrowser.getDesiredCapabilities(); setupLocalDriver(capabilities); } else { capabilities = getDesiredCapabilities(); if (localWebDriverIsUsed()) { setupLocalDriver(capabilities); } else { WebDriver dr = TestBench.createDriver(new RemoteWebDriver( new URL(getHubURL()), capabilities)); setDriver(dr); } } int w = SCREENSHOT_WIDTH; int h = SCREENSHOT_HEIGHT; if (BrowserUtil.isIE8(capabilities)) { // IE8 gets size wrong, who would have guessed... w += 4; h += 4; } try { testBench().resizeViewPortTo(w, h); } catch (UnsupportedOperationException e) { // Opera does not support this... } } protected Browser getRunLocallyBrowser() { RunLocally runLocally = getClass().getAnnotation(RunLocally.class); if (runLocally != null) { return runLocally.value(); } else { return null; } } protected WebElement getTooltipElement() { return getDriver().findElement( com.vaadin.testbench.By.className("v-tooltip-text")); } protected Coordinates getCoordinates(TestBenchElement element) { return ((Locatable) element.getWrappedElement()).getCoordinates(); } private boolean hasDebugMessage(String message) { return getDebugMessage(message) != null; } private WebElement getDebugMessage(String message) { return driver.findElement(By.xpath(String.format( "//span[@class='v-debugwindow-message' and text()='%s']", message))); } protected void waitForDebugMessage(final String expectedMessage) { waitForDebugMessage(expectedMessage, 30); } protected void waitForDebugMessage(final String expectedMessage, int timeout) { waitUntil(new ExpectedCondition() { @Override public Boolean apply(WebDriver input) { return hasDebugMessage(expectedMessage); } }, timeout); } protected void clearDebugMessages() { driver.findElement( By.xpath("//button[@class='v-debugwindow-button' and @title='Clear log']")) .click(); } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface RunLocally { public Browser value() default Browser.FIREFOX; } /** * Creates a {@link WebDriver} instance used for running the test locally * for debug purposes. Used only when {@link #runLocally()} is overridden to * return true; */ protected abstract void setupLocalDriver( DesiredCapabilities desiredCapabilities); /** * Opens the given test (defined by {@link #getTestUrl()}, optionally with * debug window and/or push (depending on {@link #isDebug()} and * {@link #isPush()}. */ protected void openTestURL() { driver.get(getTestUrl()); } /** * Returns the full URL to be used for the test * * @return the full URL for the test */ protected String getTestUrl() { String baseUrl = getBaseURL(); if (baseUrl.endsWith("/")) { baseUrl = baseUrl.substring(0, baseUrl.length() - 1); } return baseUrl + getDeploymentPath(); } /** * * @return the location (URL) of the TB hub */ protected String getHubURL() { return "http://" + getHubHostname() + ":4444/wd/hub"; } /** * Used for building the hub URL to use for the test * * @return the host name of the TestBench hub */ protected abstract String getHubHostname(); /** * Used to determine what URL to initially open for the test * * @return the host name of development server */ protected abstract String getDeploymentHostname(); /** * Used to determine what port the test is running on * * @return The port teh test is running on, by default 8888 */ protected abstract int getDeploymentPort(); /** * Produces a collection of browsers to run the test on. This method is * executed by the test runner when determining how many test methods to * invoke and with what parameters. For each returned value a test method is * ran and before running that, * {@link #setDesiredCapabilities(DesiredCapabilities)} is invoked with the * value returned by this method. * * This method is not static to allow overriding it in sub classes. By * default runs the test only on Firefox * * @return The browsers to run the test on */ public List getBrowsersToTest() { return Collections.singletonList(Browser.FIREFOX .getDesiredCapabilities()); } /** * Used to determine which capabilities should be used when setting up a * {@link WebDriver} for this test. Typically set by a test runner or left * at its default (Firefox 24). If you want to run a test on a single * browser other than Firefox 24 you can override this method. * * @return the requested browser capabilities */ protected DesiredCapabilities getDesiredCapabilities() { return desiredCapabilities; } /** * Sets the requested browser capabilities (typically browser name and * version) * * @param desiredCapabilities */ public void setDesiredCapabilities(DesiredCapabilities desiredCapabilities) { this.desiredCapabilities = desiredCapabilities; } /** * Shuts down the driver after the test has been completed * * @throws Exception */ @After public void tearDown() throws Exception { if (driver != null) { driver.quit(); } driver = null; } /** * Finds an element based on the part of a TB2 style locator following the * :: (e.g. vaadin=runLabelModes::PID_Scheckboxaction-Enabled/domChild[0] -> * PID_Scheckboxaction-Enabled/domChild[0]). * * @param vaadinLocator * The part following :: of the vaadin locator string * @return */ protected WebElement vaadinElement(String vaadinLocator) { return driver.findElement(vaadinLocator(vaadinLocator)); } /** * Uses JavaScript to determine the currently focused element. * * @return Focused element or null */ protected WebElement getFocusedElement() { Object focusedElement = ((JavascriptExecutor) getDriver()) .executeScript("return document.activeElement"); if (null != focusedElement) { return (WebElement) focusedElement; } else { return null; } } /** * Find a Vaadin element based on its id given using Component.setId * * @param id * The id to locate * @return */ public WebElement vaadinElementById(String id) { return driver.findElement(vaadinLocatorById(id)); } /** * Finds a {@link By} locator based on the part of a TB2 style locator * following the :: (e.g. * vaadin=runLabelModes::PID_Scheckboxaction-Enabled/domChild[0] -> * PID_Scheckboxaction-Enabled/domChild[0]). * * @param vaadinLocator * The part following :: of the vaadin locator string * @return */ public org.openqa.selenium.By vaadinLocator(String vaadinLocator) { String base = getApplicationId(getDeploymentPath()); base += "::"; return com.vaadin.testbench.By.vaadin(base + vaadinLocator); } /** * Constructs a {@link By} locator for the id given using Component.setId * * @param id * The id to locate * @return a locator for the given id */ public By vaadinLocatorById(String id) { return vaadinLocator("PID_S" + id); } /** * Waits up to 10s for the given condition to become true. Use e.g. as * {@link #waitUntil(ExpectedConditions.textToBePresentInElement(by, text))} * * @param condition * the condition to wait for to become true */ protected void waitUntil(ExpectedCondition condition) { waitUntil(condition, 10); } /** * Waits the given number of seconds for the given condition to become true. * Use e.g. as {@link * #waitUntil(ExpectedConditions.textToBePresentInElement(by, text))} * * @param condition * the condition to wait for to become true */ protected void waitUntil(ExpectedCondition condition, long timeoutInSeconds) { new WebDriverWait(driver, timeoutInSeconds).until(condition); } /** * Waits up to 10s for the given condition to become false. Use e.g. as * {@link #waitUntilNot(ExpectedConditions.textToBePresentInElement(by, * text))} * * @param condition * the condition to wait for to become false */ protected void waitUntilNot(ExpectedCondition condition) { waitUntilNot(condition, 10); } /** * Waits the given number of seconds for the given condition to become * false. Use e.g. as {@link * #waitUntilNot(ExpectedConditions.textToBePresentInElement(by, text))} * * @param condition * the condition to wait for to become false */ protected void waitUntilNot(ExpectedCondition condition, long timeoutInSeconds) { waitUntil(ExpectedConditions.not(condition), timeoutInSeconds); } protected void waitForElementToBePresent(By by) { waitUntil(ExpectedConditions.not(ExpectedConditions .invisibilityOfElementLocated(by))); } /** * For tests extending {@link AbstractTestUIWithLog}, returns the element * for the Nth log row * * @param rowNr * The log row to retrieve * @return the Nth log row */ protected WebElement getLogRowElement(int rowNr) { return vaadinElementById("Log_row_" + rowNr); } /** * For tests extending {@link AbstractTestUIWithLog}, returns the text in * the Nth log row * * @param rowNr * The log row to retrieve text for * @return the text in the log row */ protected String getLogRow(int rowNr) { return getLogRowElement(rowNr).getText(); } /** * Asserts that {@literal a} is >= {@literal b} * * @param message * The message to include in the {@link AssertionError} * @param a * @param b * @throws AssertionError * If comparison fails */ public static final void assertGreaterOrEqual(String message, Comparable a, T b) throws AssertionError { if (a.compareTo(b) >= 0) { return; } throw new AssertionError(decorate(message, a, b)); } /** * Asserts that {@literal a} is > {@literal b} * * @param message * The message to include in the {@link AssertionError} * @param a * @param b * @throws AssertionError * If comparison fails */ public static final void assertGreater(String message, Comparable a, T b) throws AssertionError { if (a.compareTo(b) > 0) { return; } throw new AssertionError(decorate(message, a, b)); } /** * Asserts that {@literal a} is <= {@literal b} * * @param message * The message to include in the {@link AssertionError} * @param a * @param b * @throws AssertionError * If comparison fails */ public static final void assertLessThanOrEqual(String message, Comparable a, T b) throws AssertionError { if (a.compareTo(b) <= 0) { return; } throw new AssertionError(decorate(message, a, b)); } /** * Asserts that {@literal a} is < {@literal b} * * @param message * The message to include in the {@link AssertionError} * @param a * @param b * @throws AssertionError * If comparison fails */ public static final void assertLessThan(String message, Comparable a, T b) throws AssertionError { if (a.compareTo(b) < 0) { return; } throw new AssertionError(decorate(message, a, b)); } private static String decorate(String message, Comparable a, T b) { message = message.replace("{0}", a.toString()); message = message.replace("{1}", b.toString()); return message; } /** * Returns the path that should be used for the test. The path contains the * full path (appended to hostname+port) and must start with a slash. * * @param push * true if "?debug" should be added * @param debug * true if /run-push should be used instead of /run * * @return The URL path to the UI class to test */ protected String getDeploymentPath() { Class uiClass = getUIClass(); if (uiClass != null) { return getDeploymentPath(uiClass); } throw new IllegalArgumentException("Unable to determine path for " + getClass().getCanonicalName()); } /** * Returns the UI class the current test is connected to (or in special * cases UIProvider or LegacyApplication). Uses the enclosing class if the * test class is a static inner class to a UI class. * * Test which are not enclosed by a UI class must implement this method and * return the UI class they want to test. * * Note that this method will update the test name to the enclosing class to * be compatible with TB2 screenshot naming * * @return the UI class the current test is connected to */ protected Class getUIClass() { try { // Convention: SomeUITest uses the SomeUI UI class String uiClassName = getClass().getName().replaceFirst("Test$", ""); Class cls = Class.forName(uiClassName); if (isSupportedRunnerClass(cls)) { return cls; } } catch (Exception e) { } throw new RuntimeException( "Could not determine UI class. Ensure the test is named UIClassTest and is in the same package as the UIClass"); } /** * @return true if the given class is supported by ApplicationServletRunner */ @SuppressWarnings("deprecation") private boolean isSupportedRunnerClass(Class cls) { if (UI.class.isAssignableFrom(cls)) { return true; } if (UIProvider.class.isAssignableFrom(cls)) { return true; } if (LegacyApplication.class.isAssignableFrom(cls)) { return true; } return false; } /** * Returns whether to run the test in debug mode (with the debug console * open) or not * * @return true to run with the debug window open, false by default */ protected final boolean isDebug() { return debug; } /** * Sets whether to run the test in debug mode (with the debug console open) * or not. * * @param debug * true to open debug window, false otherwise */ protected final void setDebug(boolean debug) { this.debug = debug; } /** * Returns whether to run the test with push enabled (using /run-push) or * not. Note that push tests can and should typically be created using @Push * on the UI instead of overriding this method * * @return true if /run-push is used, false otherwise */ protected final boolean isPush() { return push; } /** * Sets whether to run the test with push enabled (using /run-push) or not. * Note that push tests can and should typically be created using @Push on * the UI instead of overriding this method * * @param push * true to use /run-push in the test, false otherwise */ protected final void setPush(boolean push) { this.push = push; } /** * Returns the path for the given UI class when deployed on the test server. * The path contains the full path (appended to hostname+port) and must * start with a slash. * * This method takes into account {@link #isPush()} and {@link #isDebug()} * when the path is generated. * * @param uiClass * @param push * true if "?debug" should be added * @param debug * true if /run-push should be used instead of /run * @return The path to the given UI class */ private String getDeploymentPath(Class uiClass) { String runPath = "/run"; if (isPush()) { runPath = "/run-push"; } if (UI.class.isAssignableFrom(uiClass)) { return runPath + "/" + uiClass.getCanonicalName() + (isDebug() ? "?debug" : ""); } else if (LegacyApplication.class.isAssignableFrom(uiClass)) { return runPath + "/" + uiClass.getCanonicalName() + "?restartApplication" + (isDebug() ? "&debug" : ""); } else { throw new IllegalArgumentException( "Unable to determine path for enclosing class " + uiClass.getCanonicalName()); } } /** * Used to determine what URL to initially open for the test * * @return The base URL for the test. Does not include a trailing slash. */ protected String getBaseURL() { return "http://" + getDeploymentHostname() + ":" + getDeploymentPort(); } /** * Generates the application id based on the URL in a way compatible with * VaadinServletService. * * @param pathWithQueryParameters * The path part of the URL, possibly still containing query * parameters * @return The application ID string used in Vaadin locators */ private String getApplicationId(String pathWithQueryParameters) { // Remove any possible URL parameters String pathWithoutQueryParameters = pathWithQueryParameters.replaceAll( "\\?.*", ""); if ("".equals(pathWithoutQueryParameters)) { return "ROOT"; } // Retain only a-z and numbers return pathWithoutQueryParameters.replaceAll("[^a-zA-Z0-9]", ""); } /** * Sleeps for the given number of ms but ensures that the browser connection * does not time out. * * @param timeoutMillis * Number of ms to wait * @throws InterruptedException */ protected void sleep(int timeoutMillis) throws InterruptedException { while (timeoutMillis > 0) { int d = Math.min(BROWSER_TIMEOUT_IN_MS, timeoutMillis); Thread.sleep(d); timeoutMillis -= d; // Do something to keep the connection alive getDriver().getTitle(); } } /** * Provides helper method for selecting the browser to run on * * @author Vaadin Ltd */ public static class BrowserUtil { /** * Gets the capabilities for Safari of the given version * * @param version * the major version * @return an object describing the capabilities required for running a * test on the given Safari version */ public static DesiredCapabilities safari(int version) { DesiredCapabilities c = DesiredCapabilities.safari(); c.setPlatform(Platform.MAC); c.setVersion("" + version); return c; } /** * Gets the capabilities for Chrome of the given version * * @param version * the major version * @return an object describing the capabilities required for running a * test on the given Chrome version */ public static DesiredCapabilities chrome(int version) { DesiredCapabilities c = DesiredCapabilities.chrome(); c.setVersion("" + version); c.setPlatform(Platform.XP); return c; } /** * Gets the capabilities for Opera of the given version * * @param version * the major version * @return an object describing the capabilities required for running a * test on the given Opera version */ public static DesiredCapabilities opera(int version) { DesiredCapabilities c = DesiredCapabilities.opera(); c.setVersion("" + version); c.setPlatform(Platform.XP); return c; } /** * Gets the capabilities for Firefox of the given version * * @param version * the major version * @return an object describing the capabilities required for running a * test on the given Firefox version */ public static DesiredCapabilities firefox(int version) { DesiredCapabilities c = DesiredCapabilities.firefox(); c.setVersion("" + version); c.setPlatform(Platform.XP); return c; } /** * Gets the capabilities for Internet Explorer of the given version * * @param version * the major version * @return an object describing the capabilities required for running a * test on the given Internet Explorer version */ public static DesiredCapabilities ie(int version) { DesiredCapabilities c = DesiredCapabilities.internetExplorer(); c.setVersion("" + version); return c; } /** * Gets the capabilities for PhantomJS of the given version * * @param version * the major version * @return an object describing the capabilities required for running a * test on the given PhantomJS version */ public static DesiredCapabilities phantomJS(int version) { DesiredCapabilities c = DesiredCapabilities.phantomjs(); c.setPlatform(Platform.LINUX); c.setVersion("" + version); return c; } /** * Checks if the given capabilities refer to Internet Explorer 8 * * @param capabilities * @return true if the capabilities refer to IE8, false otherwise */ public static boolean isIE8(DesiredCapabilities capabilities) { return isIE(capabilities) && "8".equals(capabilities.getVersion()); } /** * @param capabilities * The capabilities to check * @return true if the capabilities refer to Internet Explorer, false * otherwise */ public static boolean isIE(DesiredCapabilities capabilities) { return BrowserType.IE.equals(capabilities.getBrowserName()); } /** * @param capabilities * The capabilities to check * @return true if the capabilities refer to Chrome, false otherwise */ public static boolean isChrome(DesiredCapabilities capabilities) { return BrowserType.CHROME.equals(capabilities.getBrowserName()); } /** * @param capabilities * The capabilities to check * @return true if the capabilities refer to Opera, false otherwise */ public static boolean isOpera(DesiredCapabilities capabilities) { return BrowserType.OPERA.equals(capabilities.getBrowserName()); } /** * @param capabilities * The capabilities to check * @return true if the capabilities refer to Safari, false otherwise */ public static boolean isSafari(DesiredCapabilities capabilities) { return BrowserType.SAFARI.equals(capabilities.getBrowserName()); } /** * @param capabilities * The capabilities to check * @return true if the capabilities refer to Firefox, false otherwise */ public static boolean isFirefox(DesiredCapabilities capabilities) { return BrowserType.FIREFOX.equals(capabilities.getBrowserName()); } /** * @param capabilities * The capabilities to check * @return true if the capabilities refer to PhantomJS, false otherwise */ public static boolean isPhantomJS(DesiredCapabilities capabilities) { return BrowserType.PHANTOMJS.equals(capabilities.getBrowserName()); } /** * Returns a human readable identifier of the given browser. Used for * test naming and screenshots * * @param capabilities * @return a human readable string describing the capabilities */ public static String getBrowserIdentifier( DesiredCapabilities capabilities) { if (isIE(capabilities)) { return "InternetExplorer"; } else if (isFirefox(capabilities)) { return "Firefox"; } else if (isChrome(capabilities)) { return "Chrome"; } else if (isSafari(capabilities)) { return "Safari"; } else if (isOpera(capabilities)) { return "Opera"; } else if (isPhantomJS(capabilities)) { return "PhantomJS"; } return capabilities.getBrowserName(); } /** * Returns a human readable identifier of the platform described by the * given capabilities. Used mainly for screenshots * * @param capabilities * @return a human readable string describing the platform */ public static String getPlatform(DesiredCapabilities capabilities) { if (capabilities.getPlatform() == Platform.WIN8 || capabilities.getPlatform() == Platform.WINDOWS || capabilities.getPlatform() == Platform.VISTA || capabilities.getPlatform() == Platform.XP) { return "Windows"; } else if (capabilities.getPlatform() == Platform.MAC) { return "Mac"; } return capabilities.getPlatform().toString(); } /** * Returns a string which uniquely (enough) identifies this browser. * Used mainly in screenshot names. * * @param capabilities * * @return a unique string for each browser */ public static String getUniqueIdentifier( DesiredCapabilities capabilities) { return getUniqueIdentifier(getPlatform(capabilities), getBrowserIdentifier(capabilities), capabilities.getVersion()); } /** * Returns a string which uniquely (enough) identifies this browser. * Used mainly in screenshot names. * * @param capabilities * * @return a unique string for each browser */ public static String getUniqueIdentifier( DesiredCapabilities capabilities, String versionOverride) { return getUniqueIdentifier(getPlatform(capabilities), getBrowserIdentifier(capabilities), versionOverride); } private static String getUniqueIdentifier(String platform, String browser, String version) { return platform + "_" + browser + "_" + version; } } /** * Called by the test runner whenever there is an exception in the test that * will cause termination of the test * * @param t * the throwable which caused the termination */ public void onUncaughtException(Throwable t) { // Do nothing by default } /** * Returns the mouse object for doing mouse commands * * @return Returns the mouse */ public Mouse getMouse() { return ((HasInputDevices) getDriver()).getMouse(); } /** * Returns the keyboard object for controlling keyboard events * * @return Return the keyboard */ public Keyboard getKeyboard() { return ((HasInputDevices) getDriver()).getKeyboard(); } public void hitButton(String id) { if (BrowserUtil.isPhantomJS(getDesiredCapabilities())) { driver.findElement(By.id(id)).click(); } else { WebDriverBackedSelenium selenium = new WebDriverBackedSelenium( driver, driver.getCurrentUrl()); selenium.keyPress("id=" + id, "\\13"); } } protected void openDebugLogTab() { waitUntil(new ExpectedCondition() { @Override public Boolean apply(WebDriver input) { WebElement element = getDebugLogButton(); return element != null; } }, 15); getDebugLogButton().click(); } private WebElement getDebugLogButton() { return findElement(By.xpath("//button[@title='Debug message log']")); } }