diff options
author | Henri Sara <hesara@vaadin.com> | 2016-06-07 13:14:29 +0300 |
---|---|---|
committer | Vaadin Code Review <review@vaadin.com> | 2016-08-17 12:13:50 +0000 |
commit | 3816d34daaf9eca4b3b591b79309a98796b3a4da (patch) | |
tree | 9a478b861310c00cdc750790120d9eea56a2a580 /uitest-common | |
parent | c89f50eb801a903ec28c8618cf30ae31b9184c8f (diff) | |
download | vaadin-framework-3816d34daaf9eca4b3b591b79309a98796b3a4da.tar.gz vaadin-framework-3816d34daaf9eca4b3b591b79309a98796b3a4da.zip |
Create module uitest-common
This module will contain most of the classes shared between uitest
and integration test modules.
Change-Id: Ibc327339cf1d360252483287c0eeb8dd03c176d5
Diffstat (limited to 'uitest-common')
19 files changed, 3585 insertions, 0 deletions
diff --git a/uitest-common/pom.xml b/uitest-common/pom.xml new file mode 100644 index 0000000000..d46927fd2a --- /dev/null +++ b/uitest-common/pom.xml @@ -0,0 +1,114 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>com.vaadin</groupId> + <artifactId>vaadin-root</artifactId> + <version>8.0-SNAPSHOT</version> + </parent> + <artifactId>vaadin-uitest-common</artifactId> + <name>vaadin-uitest-common</name> + <packaging>jar</packaging> + <properties> + <skip.uitest.deployment>true</skip.uitest.deployment> + </properties> + + <url>https://vaadin.com/</url> + <description>Vaadin UI test common classes and dependencies</description> + + <repositories> + <repository> + <id>vaadin-addons</id> + <url>http://maven.vaadin.com/vaadin-addons</url> + </repository> + </repositories> + + <dependencies> + <!-- LIBRARY DEPENDENCIES (compile time) --> + <dependency> + <groupId>com.vaadin</groupId> + <artifactId>vaadin-server</artifactId> + <version>${project.version}</version> + <exclusions> + <exclusion> + <groupId>javax.servlet</groupId> + <artifactId>javax.servlet-api</artifactId> + </exclusion> + </exclusions> + </dependency> + + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.11</version> + </dependency> + + <dependency> + <groupId>commons-io</groupId> + <artifactId>commons-io</artifactId> + <version>${commons-io.version}</version> + </dependency> + + <dependency> + <groupId>com.vaadin</groupId> + <artifactId>vaadin-testbench</artifactId> + <version>4.0.3</version> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <artifactId>maven-deploy-plugin</artifactId> + <configuration> + <skip>${skip.uitest.deployment}</skip> + </configuration> + </plugin> + <plugin> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <skip>true</skip> + </configuration> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>animal-sniffer-maven-plugin</artifactId> + <configuration> + <skip>true</skip> + </configuration> + </plugin> + </plugins> + <pluginManagement> + <plugins> + <plugin> + <groupId>org.sonatype.plugins</groupId> + <artifactId>nexus-staging-maven-plugin</artifactId> + <configuration> + <skipNexusStagingDeployMojo>true</skipNexusStagingDeployMojo> + </configuration> + </plugin> + <plugin> + <artifactId>maven-javadoc-plugin</artifactId> + <configuration> + <skip>true</skip> + </configuration> + </plugin> + </plugins> + </pluginManagement> + </build> + + <profiles> + <profile> + <!-- Profile used to deploy the uitest-common jar for testing --> + <id>dev-build</id> + <activation> + <activeByDefault>false</activeByDefault> + </activation> + <properties> + <skip.uitest.deployment>false</skip.uitest.deployment> + </properties> + </profile> + </profiles> + +</project> diff --git a/uitest-common/src/main/java/com/vaadin/tests/tb3/AbstractTB3Test.java b/uitest-common/src/main/java/com/vaadin/tests/tb3/AbstractTB3Test.java new file mode 100644 index 0000000000..9c617ab128 --- /dev/null +++ b/uitest-common/src/main/java/com/vaadin/tests/tb3/AbstractTB3Test.java @@ -0,0 +1,1262 @@ +/* + * 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.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.lang.reflect.Field; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicHttpEntityEnclosingRequest; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; +import org.openqa.selenium.By; +import org.openqa.selenium.Dimension; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.interactions.Actions; +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.internal.WrapsElement; +import org.openqa.selenium.remote.DesiredCapabilities; +import org.openqa.selenium.remote.HttpCommandExecutor; +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.TestBenchDriverProxy; +import com.vaadin.testbench.TestBenchElement; +import com.vaadin.testbench.annotations.BrowserConfiguration; +import com.vaadin.testbench.elements.CheckBoxElement; +import com.vaadin.testbench.elements.LabelElement; +import com.vaadin.testbench.elements.TableElement; +import com.vaadin.testbench.elements.VerticalLayoutElement; +import com.vaadin.testbench.parallel.Browser; +import com.vaadin.testbench.parallel.BrowserUtil; +import com.vaadin.testbench.parallel.ParallelTest; +import com.vaadin.ui.UI; + +import elemental.json.JsonObject; +import elemental.json.impl.JsonUtil; + +/** + * Base class for TestBench 3+ tests. All TB3+ tests in the project should + * extend this class. + * + * Provides: + * <ul> + * <li>Helpers for browser selection</li> + * <li>Hub connection setup and teardown</li> + * <li>Automatic generation of URL for a given test on the development server + * using {@link #getUIClass()} or by automatically finding an enclosing UI class + * and based on requested features, e.g. {@link #isDebug()}, {@link #isPush()}</li> + * <li>Generic helpers for creating TB3+ tests</li> + * </ul> + * + * @author Vaadin Ltd + */ +@RunWith(TB3Runner.class) +public abstract class AbstractTB3Test extends ParallelTest { + + @Rule + public TestName testName = new TestName(); + + @Rule + public RetryOnFail retry = new RetryOnFail(); + + /** + * 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; + + protected static DesiredCapabilities PHANTOMJS2() { + DesiredCapabilities phantomjs2 = new VaadinBrowserFactory().create( + Browser.PHANTOMJS, "2"); + // Hack for the test cluster + phantomjs2 + .setCapability("phantomjs.binary.path", "/usr/bin/phantomjs2"); + return phantomjs2; + } + + private boolean debug = false; + + private boolean push = false; + + static { + com.vaadin.testbench.Parameters + .setScreenshotComparisonCursorDetection(true); + } + + /** + * 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 + */ + @Override + public void setup() throws Exception { + super.setup(); + + int w = SCREENSHOT_WIDTH; + int h = SCREENSHOT_HEIGHT; + + if (BrowserUtil.isIE8(super.getDesiredCapabilities())) { + // 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... + } + } + + /** + * Method for closing the tested application. + */ + protected void closeApplication() { + if (driver != null) { + try { + openTestURL("closeApplication"); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + protected WebElement getTooltipErrorElement() { + WebElement tooltip = getDriver().findElement( + com.vaadin.testbench.By.className("v-tooltip")); + return tooltip.findElement(By.className("v-errormessage")); + } + + 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<Boolean>() { + + @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(); + } + + protected void waitUntilRowIsVisible(final TableElement table, final int row) { + waitUntil(new ExpectedCondition<Object>() { + @Override + public Object apply(WebDriver input) { + try { + return table.getCell(row, 0) != null; + } catch (NoSuchElementException e) { + return false; + } + } + }); + } + + protected void scrollTable(TableElement table, int rows, int rowToWait) { + testBenchElement(table.findElement(By.className("v-scrollable"))) + .scroll(rows * 30); + + waitUntilRowIsVisible(table, rowToWait); + } + + /** + * 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() { + openTestURL(new String[0]); + } + + /** + * 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(String... parameters) { + openTestURL(getUIClass(), parameters); + } + + /** + * 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(Class<?> uiClass, String... parameters) { + openTestURL(uiClass, new HashSet<String>(Arrays.asList(parameters))); + } + + private void openTestURL(Class<?> uiClass, Set<String> parameters) { + String url = getTestURL(uiClass); + + if (isDebug()) { + parameters.add("debug"); + } + + if (LegacyApplication.class.isAssignableFrom(uiClass)) { + parameters.add("restartApplication"); + } + + if (parameters.size() > 0) { + url += "?" + StringUtils.join(parameters, "&"); + } + + driver.get(url); + } + + /** + * Returns the full URL to be used for the test + * + * @return the full URL for the test + */ + protected String getTestUrl() { + return StringUtils.strip(getBaseURL(), "/") + getDeploymentPath(); + } + + /** + * Returns the full URL to be used for the test for the provided UI class. + * + * @return the full URL for the test + */ + protected String getTestURL(Class<?> uiClass) { + return StringUtils.strip(getBaseURL(), "/") + + getDeploymentPath(uiClass); + } + + /** + * 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 + */ + @BrowserConfiguration + public List<DesiredCapabilities> getBrowsersToTest() { + return Collections.singletonList(Browser.FIREFOX + .getDesiredCapabilities()); + } + + /** + * 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 = executeScript("return document.activeElement"); + if (null != focusedElement) { + return (WebElement) focusedElement; + } else { + return null; + } + } + + /** + * Executes the given Javascript + * + * @param script + * the script to execute + * @return whatever + * {@link org.openqa.selenium.JavascriptExecutor#executeScript(String, Object...)} + * returns + */ + protected Object executeScript(String script, Object... args) { + return ((JavascriptExecutor) getDriver()).executeScript(script, args); + } + + /** + * 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(By.id(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 <T> void waitUntil(ExpectedCondition<T> 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 <T> void waitUntil(ExpectedCondition<T> 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 <T> void waitUntilNot(ExpectedCondition<T> 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 <T> void waitUntilNot(ExpectedCondition<T> condition, + long timeoutInSeconds) { + waitUntil(ExpectedConditions.not(condition), timeoutInSeconds); + } + + protected void waitForElementPresent(final By by) { + waitUntil(ExpectedConditions.presenceOfElementLocated(by)); + } + + protected void waitForElementNotPresent(final By by) { + waitUntil(new ExpectedCondition<Boolean>() { + @Override + public Boolean apply(WebDriver input) { + return input.findElements(by).isEmpty(); + } + }); + } + + protected void waitForElementVisible(final By by) { + waitUntil(ExpectedConditions.visibilityOfElementLocated(by)); + } + + /** + * Checks if the given element has the given class name. + * + * Matches only full class names, i.e. has ("foo") does not match + * class="foobar" + * + * @param element + * @param className + * @return + */ + protected boolean hasCssClass(WebElement element, String className) { + String classes = element.getAttribute("class"); + if (classes == null || classes.isEmpty()) { + return (className == null || className.isEmpty()); + } + + for (String cls : classes.split(" ")) { + if (className.equals(cls)) { + return true; + } + } + + return false; + } + + /** + * For tests extending 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 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 <T> void assertGreaterOrEqual(String message, + Comparable<T> 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 <T> void assertGreater(String message, Comparable<T> 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 <T> void assertLessThanOrEqual(String message, + Comparable<T> 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 <T> void assertLessThan(String message, + Comparable<T> a, T b) throws AssertionError { + if (a.compareTo(b) < 0) { + return; + } + throw new AssertionError(decorate(message, a, b)); + } + + private static <T> String decorate(String message, Comparable<T> 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 + */ + protected String getDeploymentPath(Class<?> uiClass) { + String runPath = "/run"; + if (isPush()) { + runPath = "/run-push"; + } + + if (UI.class.isAssignableFrom(uiClass) + || UIProvider.class.isAssignableFrom(uiClass) + || LegacyApplication.class.isAssignableFrom(uiClass)) { + return runPath + "/" + uiClass.getCanonicalName(); + } 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(); + } + } + + /** + * 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<Boolean>() { + @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']")); + } + + protected void assertNoDebugMessage(Level level) { + // class="v-debugwindow-row Level.getName()" + List<WebElement> logElements = driver + .findElements(By.xpath(String + .format("//div[@class='v-debugwindow-row %s']/span[@class='v-debugwindow-message']", + level.getName()))); + if (!logElements.isEmpty()) { + String logRows = ""; + for (WebElement e : logElements) { + logRows += "\n" + e.getText(); + } + Assert.fail("Found debug messages with level " + level.getName() + + ": " + logRows); + } + } + + /** + * Should the "require window focus" be enabled for Internet Explorer. + * RequireWindowFocus makes tests more stable but seems to be broken with + * certain commands such as sendKeys. Therefore it is not enabled by default + * for all tests + * + * @return true, to use the "require window focus" feature, false otherwise + */ + protected boolean requireWindowFocusForIE() { + return false; + } + + /** + * Should the "enable persistent hover" be enabled for Internet Explorer. + * + * Persistent hovering causes continuous firing of mouse over events at the + * last location the mouse cursor has been moved to. This is to avoid + * problems where the real mouse cursor is inside the browser window and + * Internet Explorer uses that location for some undefined operation + * (http:// + * jimevansmusic.blogspot.fi/2012/06/whats-wrong-with-internet-explorer + * .html) + * + * @return true, to use the "persistent hover" feature, false otherwise + */ + protected boolean usePersistentHoverForIE() { + return true; + } + + /** + * Should the "native events" be enabled for Internet Explorer. + * <p> + * Native events sometimes cause failure in clicking on buttons/checkboxes + * but are possibly needed for some operations. + * + * @return true, to use "native events", false to use generated Javascript + * events + */ + protected boolean useNativeEventsForIE() { + return true; + } + + // FIXME: Remove this once TB4 getRemoteControlName works properly + private RemoteWebDriver getRemoteDriver() { + WebDriver d = getDriver(); + if (d instanceof TestBenchDriverProxy) { + try { + Field f = TestBenchDriverProxy.class + .getDeclaredField("actualDriver"); + f.setAccessible(true); + return (RemoteWebDriver) f.get(d); + } catch (Exception e) { + e.printStackTrace(); + } + } + + if (d instanceof RemoteWebDriver) { + return (RemoteWebDriver) d; + } + + return null; + + } + + // FIXME: Remove this once TB4 getRemoteControlName works properly + protected String getRemoteControlName() { + try { + RemoteWebDriver d = getRemoteDriver(); + if (d == null) { + return null; + } + HttpCommandExecutor ce = (HttpCommandExecutor) d + .getCommandExecutor(); + String hostName = ce.getAddressOfRemoteServer().getHost(); + int port = ce.getAddressOfRemoteServer().getPort(); + HttpHost host = new HttpHost(hostName, port); + DefaultHttpClient client = new DefaultHttpClient(); + URL sessionURL = new URL("http://" + hostName + ":" + port + + "/grid/api/testsession?session=" + d.getSessionId()); + BasicHttpEntityEnclosingRequest r = new BasicHttpEntityEnclosingRequest( + "POST", sessionURL.toExternalForm()); + HttpResponse response = client.execute(host, r); + JsonObject object = extractObject(response); + URL myURL = new URL(object.getString("proxyId")); + if ((myURL.getHost() != null) && (myURL.getPort() != -1)) { + return myURL.getHost(); + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + protected boolean logContainsText(String string) { + List<String> logs = getLogs(); + + for (String text : logs) { + if (text.contains(string)) { + return true; + } + } + + return false; + } + + protected List<String> getLogs() { + VerticalLayoutElement log = $(VerticalLayoutElement.class).id("Log"); + List<LabelElement> logLabels = log.$(LabelElement.class).all(); + List<String> logTexts = new ArrayList<String>(); + + for (LabelElement label : logLabels) { + logTexts.add(label.getText()); + } + + return logTexts; + } + + private static JsonObject extractObject(HttpResponse resp) + throws IOException { + InputStream contents = resp.getEntity().getContent(); + StringWriter writer = new StringWriter(); + IOUtils.copy(contents, writer, "UTF8"); + return JsonUtil.parse(writer.toString()); + } + + protected void click(CheckBoxElement checkbox) { + WebElement cb = checkbox.findElement(By.xpath("input")); + if (BrowserUtil.isChrome(getDesiredCapabilities())) { + testBenchElement(cb).click(0, 0); + } else { + cb.click(); + } + } + + protected boolean isLoadingIndicatorVisible() { + WebElement loadingIndicator = findElement(By + .className("v-loading-indicator")); + + return loadingIndicator.isDisplayed(); + } + + protected void waitUntilLoadingIndicatorNotVisible() { + waitUntil(new ExpectedCondition<Boolean>() { + + @Override + public Boolean apply(WebDriver input) { + WebElement loadingIndicator = input.findElement(By + .className("v-loading-indicator")); + + return !loadingIndicator.isDisplayed(); + } + }); + } + + /** + * Selects a menu item. By default, this will click on the menu item. + * + * @param menuCaption + * caption of the menu item + */ + protected void selectMenu(String menuCaption) { + selectMenu(menuCaption, true); + } + + /** + * Selects a menu item. + * + * @param menuCaption + * caption of the menu item + * @param click + * <code>true</code> if should click the menu item; + * <code>false</code> if not + */ + protected void selectMenu(String menuCaption, boolean click) { + WebElement menuElement = getMenuElement(menuCaption); + Dimension size = menuElement.getSize(); + new Actions(getDriver()).moveToElement(menuElement, size.width - 10, + size.height / 2).perform(); + if (click) { + new Actions(getDriver()).click().perform(); + } + } + + /** + * Finds the menu item from the DOM based on menu item caption. + * + * @param menuCaption + * caption of the menu item + * @return the found menu item + * @throws NoSuchElementException + * if menu item is not found + */ + protected WebElement getMenuElement(String menuCaption) + throws NoSuchElementException { + return getDriver().findElement( + By.xpath("//span[text() = '" + menuCaption + "']")); + } + + /** + * Selects a submenu described by a path of menus from the first MenuBar in + * the UI. + * + * @param menuCaptions + * array of menu captions + */ + protected void selectMenuPath(String... menuCaptions) { + selectMenu(menuCaptions[0], true); + + // Move to the menu item opened below the menu bar. + new Actions(getDriver()).moveByOffset(0, + getMenuElement(menuCaptions[0]).getSize().getHeight()) + .perform(); + + for (int i = 1; i < menuCaptions.length - 1; i++) { + selectMenu(menuCaptions[i]); + new Actions(getDriver()).moveByOffset(40, 0).build().perform(); + } + selectMenu(menuCaptions[menuCaptions.length - 1], true); + } + + /** + * Asserts that an element is present + * + * @param by + * the locatore for the element + */ + protected void assertElementPresent(By by) { + Assert.assertTrue("Element is not present", isElementPresent(by)); + } + + /** + * Asserts that an element is not present + * + * @param by + * the locatore for the element + */ + protected void assertElementNotPresent(By by) { + Assert.assertFalse("Element is present", isElementPresent(by)); + } + + /** + * Asserts that no error notifications are shown. Requires the use of + * "?debug" as exceptions are otherwise not shown as notifications. + */ + protected void assertNoErrorNotifications() { + Assert.assertFalse( + "Error notification with client side exception is shown", + isNotificationPresent("error")); + } + + /** + * Asserts that no system notifications are shown. + */ + protected void assertNoSystemNotifications() { + Assert.assertFalse( + "Error notification with system error exception is shown", + isNotificationPresent("system")); + } + + /** + * Asserts that a system notification is shown. + */ + protected void assertSystemNotification() { + Assert.assertTrue( + "Error notification with system error exception is not shown", + isNotificationPresent("system")); + } + + private boolean isNotificationPresent(String type) { + if ("error".equals(type)) { + Assert.assertTrue( + "Debug window must be open to be able to see error notifications", + isDebugWindowOpen()); + } + return isElementPresent(By.className("v-Notification-" + type)); + } + + private boolean isDebugWindowOpen() { + return isElementPresent(By.className("v-debugwindow")); + } + + protected void assertNoHorizontalScrollbar(WebElement element, + String errorMessage) { + // IE rounds clientWidth/clientHeight down and scrollHeight/scrollWidth + // up, so using clientWidth/clientHeight will fail if the element height + // is not an integer + int clientWidth = getClientWidth(element); + int scrollWidth = getScrollWidth(element); + boolean hasScrollbar = scrollWidth > clientWidth; + + Assert.assertFalse( + "The element should not have a horizontal scrollbar (scrollWidth: " + + scrollWidth + ", clientWidth: " + clientWidth + "): " + + errorMessage, hasScrollbar); + } + + protected void assertNoVerticalScrollbar(WebElement element, + String errorMessage) { + // IE rounds clientWidth/clientHeight down and scrollHeight/scrollWidth + // up, so using clientWidth/clientHeight will fail if the element height + // is not an integer + int clientHeight = getClientHeight(element); + int scrollHeight = getScrollHeight(element); + boolean hasScrollbar = scrollHeight > clientHeight; + + Assert.assertFalse( + "The element should not have a vertical scrollbar (scrollHeight: " + + scrollHeight + ", clientHeight: " + clientHeight + + "): " + errorMessage, hasScrollbar); + } + + protected int getScrollHeight(WebElement element) { + return ((Number) executeScript("return arguments[0].scrollHeight;", + element)).intValue(); + } + + protected int getScrollWidth(WebElement element) { + return ((Number) executeScript("return arguments[0].scrollWidth;", + element)).intValue(); + } + + /** + * Returns client height rounded up instead of as double because of IE9 + * issues: https://dev.vaadin.com/ticket/18469 + */ + protected int getClientHeight(WebElement e) { + String script; + if (BrowserUtil.isIE8(getDesiredCapabilities())) { + script = "return arguments[0].clientHeight;"; // + } else { + script = "var cs = window.getComputedStyle(arguments[0]);" + + "return Math.ceil(parseFloat(cs.height)+parseFloat(cs.paddingTop)+parseFloat(cs.paddingBottom));"; + } + return ((Number) executeScript(script, e)).intValue(); + } + + /** + * Returns client width rounded up instead of as double because of IE9 + * issues: https://dev.vaadin.com/ticket/18469 + */ + protected int getClientWidth(WebElement e) { + String script; + if (BrowserUtil.isIE8(getDesiredCapabilities())) { + script = "return arguments[0].clientWidth;"; + } else { + script = "var cs = window.getComputedStyle(arguments[0]);" + + "var h = parseFloat(cs.width)+parseFloat(cs.paddingLeft)+parseFloat(cs.paddingRight);" + + "return Math.ceil(h);"; + } + + return ((Number) executeScript(script, e)).intValue(); + } + + protected void assertElementsEquals(WebElement expectedElement, + WebElement actualElement) { + while (expectedElement instanceof WrapsElement) { + expectedElement = ((WrapsElement) expectedElement) + .getWrappedElement(); + } + while (actualElement instanceof WrapsElement) { + actualElement = ((WrapsElement) actualElement).getWrappedElement(); + } + + Assert.assertEquals(expectedElement, actualElement); + } + + protected WebElement getActiveElement() { + return (WebElement) executeScript("return document.activeElement;"); + + } + + protected void waitForThemeToChange(final String theme) { + + final WebElement rootDiv = findElement(By + .xpath("//div[contains(@class,'v-app')]")); + waitUntil(new ExpectedCondition<Boolean>() { + + @Override + public Boolean apply(WebDriver input) { + String rootClass = rootDiv.getAttribute("class").trim(); + + return rootClass.contains(theme); + } + }, 30); + } + +} diff --git a/uitest-common/src/main/java/com/vaadin/tests/tb3/BrowserStackBrowserFactory.java b/uitest-common/src/main/java/com/vaadin/tests/tb3/BrowserStackBrowserFactory.java new file mode 100644 index 0000000000..ab9f7018eb --- /dev/null +++ b/uitest-common/src/main/java/com/vaadin/tests/tb3/BrowserStackBrowserFactory.java @@ -0,0 +1,135 @@ +/* + * 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.util.logging.Logger; + +import org.openqa.selenium.Platform; +import org.openqa.selenium.ie.InternetExplorerDriver; +import org.openqa.selenium.remote.DesiredCapabilities; + +import com.vaadin.shared.Version; +import com.vaadin.testbench.parallel.Browser; +import com.vaadin.testbench.parallel.DefaultBrowserFactory; + +/** + * Browser factory for the cloud test provider BrowserStack. + */ +public class BrowserStackBrowserFactory extends DefaultBrowserFactory { + + @Override + public DesiredCapabilities create(Browser browser, String version, + Platform platform) { + DesiredCapabilities caps; + + switch (browser) { + case CHROME: + caps = DesiredCapabilities.chrome(); + caps.setVersion(version); + break; + case PHANTOMJS: + // This will not work on BrowserStack - should be filtered with + // browsers.exclude. However, we cannot throw an exception here as + // filtering only takes place if there is no exception. + caps = DesiredCapabilities.phantomjs(); + caps.setVersion("1"); + caps.setPlatform(Platform.LINUX); + break; + case SAFARI: + caps = DesiredCapabilities.safari(); + caps.setVersion(version); + break; + case IE8: + caps = DesiredCapabilities.internetExplorer(); + caps.setVersion("8"); + caps.setCapability("browser", "IE"); + caps.setCapability("browser_version", "8.0"); + caps.setCapability(InternetExplorerDriver.IE_ENSURE_CLEAN_SESSION, + true); + break; + case IE9: + caps = DesiredCapabilities.internetExplorer(); + caps.setVersion("9"); + caps.setCapability("browser", "IE"); + caps.setCapability("browser_version", "9.0"); + caps.setCapability(InternetExplorerDriver.IE_ENSURE_CLEAN_SESSION, + true); + break; + case IE10: + caps = DesiredCapabilities.internetExplorer(); + caps.setVersion("10"); + caps.setCapability("browser", "IE"); + caps.setCapability("browser_version", "10.0"); + caps.setCapability(InternetExplorerDriver.IE_ENSURE_CLEAN_SESSION, + true); + break; + case IE11: + caps = DesiredCapabilities.internetExplorer(); + caps.setVersion("11"); + caps.setCapability("browser", "IE"); + caps.setCapability("browser_version", "11.0"); + // There are 2 capabilities ie.ensureCleanSession and + // ensureCleanSession in Selenium + // IE 11 uses ie.ensureCleanSession + caps.setCapability("ie.ensureCleanSession", true); + caps.setCapability(InternetExplorerDriver.IE_ENSURE_CLEAN_SESSION, + true); + break; + case FIREFOX: + caps = DesiredCapabilities.firefox(); + caps.setVersion(version); + break; + default: + caps = DesiredCapabilities.firefox(); + caps.setVersion(version); + caps.setPlatform(platform); + } + + // BrowserStack specific parts + + // for now, run all tests on Windows 7 + if (!Browser.PHANTOMJS.equals(browser)) { + caps.setCapability("os", "Windows"); + caps.setCapability("os_version", "7"); + caps.setPlatform(Platform.WINDOWS); + } + + // enable logging on BrowserStack + caps.setCapability("browserstack.debug", "true"); + + // tunnel + caps.setCapability("browserstack.local", "true"); + // optionally, could also set browserstack.localIdentifier if we have a + // tunnel name + + // build and project for easy identification in BrowserStack UI + caps.setCapability("project", "vaadin"); + caps.setCapability("build", Version.getFullVersion()); + + // accept self-signed certificates + caps.setCapability("acceptSslCerts", "true"); + + caps.setCapability("resolution", "1680x1050"); + + getLogger().info("Using BrowserStack capabilities " + caps); + + return caps; + } + + private static final Logger getLogger() { + return Logger.getLogger(BrowserStackBrowserFactory.class.getName()); + } +} diff --git a/uitest-common/src/main/java/com/vaadin/tests/tb3/CustomTestBenchCommandExecutor.java b/uitest-common/src/main/java/com/vaadin/tests/tb3/CustomTestBenchCommandExecutor.java new file mode 100644 index 0000000000..00d7788f8b --- /dev/null +++ b/uitest-common/src/main/java/com/vaadin/tests/tb3/CustomTestBenchCommandExecutor.java @@ -0,0 +1,165 @@ +/* + * 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.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.util.logging.Logger; + +import javax.imageio.IIOException; +import javax.imageio.ImageIO; + +import org.openqa.selenium.Dimension; +import org.openqa.selenium.OutputType; +import org.openqa.selenium.Point; +import org.openqa.selenium.TakesScreenshot; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; + +import com.vaadin.testbench.Parameters; +import com.vaadin.testbench.screenshot.ImageComparison; +import com.vaadin.testbench.screenshot.ImageFileUtil; + +/** + * Internal hack to support capturing screenshots for elements. + * + * Most parts are from TestBenchCommandExecutor and the feature should be + * integrated into TB4. + * + * @author Vaadin Ltd + */ +public class CustomTestBenchCommandExecutor { + + private ImageComparison imageComparison = new ImageComparison(); + private final WebDriver actualDriver; + + public CustomTestBenchCommandExecutor(WebDriver driver) { + actualDriver = driver; + } + + /** + * Compares the screenshot of the given element with the reference. + * + * Copied from TestBenchCommandExecutor + */ + public boolean compareScreen(WebElement element, File reference, + boolean isIE8) throws IOException { + BufferedImage image = null; + try { + image = ImageIO.read(reference); + } catch (IIOException e) { + // Don't worry, an error screen shot will be generated that later + // can be used as the reference + } + return compareScreen(element, image, reference.getName(), isIE8); + } + + /** + * Compares the screenshot of the given element with the reference. + * + * Copied from TestBenchCommandExecutor and added cropToElement + */ + public boolean compareScreen(WebElement element, BufferedImage reference, + String referenceName, boolean isIE8) throws IOException { + for (int times = 0; times < Parameters.getMaxScreenshotRetries(); times++) { + BufferedImage screenshotImage = cropToElement(element, + ImageIO.read(new ByteArrayInputStream( + ((TakesScreenshot) actualDriver) + .getScreenshotAs(OutputType.BYTES))), isIE8); + if (reference == null) { + // Store the screenshot in the errors directory and fail the + // test + ImageFileUtil.createScreenshotDirectoriesIfNeeded(); + ImageIO.write(screenshotImage, "png", + ImageFileUtil.getErrorScreenshotFile(referenceName)); + getLogger().severe( + "No reference found for " + + referenceName + + " in " + + ImageFileUtil + .getScreenshotReferenceDirectory()); + return false; + } + if (imageComparison.imageEqualToReference(screenshotImage, + reference, referenceName, + Parameters.getScreenshotComparisonTolerance())) { + return true; + } + pause(Parameters.getScreenshotRetryDelay()); + } + return false; + } + + /** + * Crops the image to show only the element. If the element is partly off + * screen, crops to show the part of the element which is in the screenshot + * + * @param element + * the element to retain in the screenshot + * @param fullScreen + * the full screen image + * @param isIE8 + * true if the browser is IE8 + * @return + * @throws IOException + */ + public static BufferedImage cropToElement(WebElement element, + BufferedImage fullScreen, boolean isIE8) throws IOException { + Point loc = element.getLocation(); + Dimension size = element.getSize(); + int x = loc.x, y = loc.y; + int w = size.width; + int h = size.height; + + if (isIE8) { + // IE8 border... + x += 2; + y += 2; + } + if (x >= 0 && x < fullScreen.getWidth()) { + // X loc on screen + // Get the part of the element which is on screen + w = Math.min(fullScreen.getWidth() - x, w); + } else { + throw new IOException("Element x is outside the screenshot (x: " + + x + ", y: " + y + ")"); + } + + if (y >= 0 && y < fullScreen.getHeight()) { + // Y loc on screen + // Get the part of the element which is on screen + h = Math.min(fullScreen.getHeight() - y, h); + } else { + throw new IOException("Element y is outside the screenshot (x: " + + x + ", y: " + y + ")"); + } + + return fullScreen.getSubimage(x, y, w, h); + } + + private void pause(int delay) { + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + } + } + + private static Logger getLogger() { + return Logger.getLogger(CustomTestBenchCommandExecutor.class.getName()); + } +} diff --git a/uitest-common/src/main/java/com/vaadin/tests/tb3/ExcludeFromSuite.java b/uitest-common/src/main/java/com/vaadin/tests/tb3/ExcludeFromSuite.java new file mode 100644 index 0000000000..dd061646be --- /dev/null +++ b/uitest-common/src/main/java/com/vaadin/tests/tb3/ExcludeFromSuite.java @@ -0,0 +1,37 @@ +/* + * 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.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marker interface for a TB3+ test class which will exclude the test from any + * test suite which automatically scans for test classes. Mostly useful for long + * tests which should not be run in every build. + * + * @since 7.1.10 + * @author Vaadin Ltd + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface ExcludeFromSuite { + +} diff --git a/uitest-common/src/main/java/com/vaadin/tests/tb3/IncludeIfProperty.java b/uitest-common/src/main/java/com/vaadin/tests/tb3/IncludeIfProperty.java new file mode 100644 index 0000000000..789422c0c6 --- /dev/null +++ b/uitest-common/src/main/java/com/vaadin/tests/tb3/IncludeIfProperty.java @@ -0,0 +1,44 @@ +/* + * 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.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to control inclusion of a test into a test suite. + * <p> + * The test will be included in the suite only if the given System property + * {@code property} has the given {@code value}. + * <p> + * Used by {@link TB3TestLocator} + * + * @since + * @author Vaadin Ltd + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface IncludeIfProperty { + + String property(); + + String value(); + +} diff --git a/uitest-common/src/main/java/com/vaadin/tests/tb3/MultiBrowserTest.java b/uitest-common/src/main/java/com/vaadin/tests/tb3/MultiBrowserTest.java new file mode 100644 index 0000000000..ec40510d96 --- /dev/null +++ b/uitest-common/src/main/java/com/vaadin/tests/tb3/MultiBrowserTest.java @@ -0,0 +1,116 @@ +/* + * 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.util.ArrayList; +import java.util.List; + +import org.openqa.selenium.remote.DesiredCapabilities; + +import com.vaadin.testbench.parallel.Browser; + +/** + * Base class for tests which should be run on all supported browsers. The test + * is automatically launched for multiple browsers in parallel by the test + * runner. + * + * Sub classes can, but typically should not, restrict the browsers used by + * implementing a + * + * <pre> + * @Parameters + * public static Collection<DesiredCapabilities> getBrowsersForTest() { + * } + * </pre> + * + * @author Vaadin Ltd + */ +public abstract class MultiBrowserTest extends PrivateTB3Configuration { + + protected List<DesiredCapabilities> getBrowsersSupportingWebSocket() { + // No WebSocket support in IE8-9 and PhantomJS + return getBrowserCapabilities(Browser.IE10, Browser.IE11, + Browser.FIREFOX, Browser.CHROME); + } + + protected List<DesiredCapabilities> getBrowsersExcludingPhantomJS() { + return getBrowserCapabilities(Browser.IE8, Browser.IE9, Browser.IE10, + Browser.IE11, Browser.CHROME, Browser.FIREFOX); + } + + protected List<DesiredCapabilities> getBrowsersExcludingIE() { + return getBrowserCapabilities(Browser.FIREFOX, Browser.CHROME, + Browser.PHANTOMJS); + } + + protected List<DesiredCapabilities> getBrowsersExcludingFirefox() { + // this is sometimes needed as the Firefox driver causes extra mouseOut + // events that make tooltips disappear etc. + return getBrowserCapabilities(Browser.IE8, Browser.IE9, Browser.IE10, + Browser.IE11, Browser.CHROME, Browser.PHANTOMJS); + } + + protected List<DesiredCapabilities> getBrowsersExcludingIE8() { + return getBrowserCapabilities(Browser.IE9, Browser.IE10, Browser.IE11, + Browser.FIREFOX, Browser.CHROME, Browser.PHANTOMJS); + } + + protected List<DesiredCapabilities> getBrowsersSupportingShiftClick() { + return getBrowserCapabilities(Browser.IE8, Browser.IE9, Browser.IE10, + Browser.IE11, Browser.CHROME); + } + + protected List<DesiredCapabilities> getIEBrowsersOnly() { + return getBrowserCapabilities(Browser.IE8, Browser.IE9, Browser.IE10, + Browser.IE11); + } + + protected List<DesiredCapabilities> getBrowsersSupportingContextMenu() { + // context menu doesn't work in phantom JS and works weirdly with IE8 + // and selenium. + return getBrowserCapabilities(Browser.IE9, Browser.IE10, Browser.IE11, + Browser.FIREFOX, Browser.CHROME); + } + + protected List<DesiredCapabilities> getBrowsersSupportingTooltip() { + // With IEDriver, the cursor seems to jump to default position after the + // mouse move, so we are not able to test the tooltip behaviour properly + // unless using requireWindowFocusForIE() { return true; } . + // See #13854. + // On Firefox, the driver causes additional mouseOut events causing the + // tooltip to disappear immediately. Tooltips may work in some + // particular cases, but not in general. + return getBrowserCapabilities(Browser.CHROME, Browser.PHANTOMJS); + } + + @Override + public List<DesiredCapabilities> getBrowsersToTest() { + // Uncomment Safari and Opera if those become tested browsers again. + return getBrowserCapabilities(Browser.IE8, Browser.IE9, Browser.IE10, + Browser.IE11, Browser.FIREFOX, Browser.CHROME, + Browser.PHANTOMJS /* , Browser.SAFARI, Browser.OPERA */); + } + + protected List<DesiredCapabilities> getBrowserCapabilities( + Browser... browsers) { + List<DesiredCapabilities> capabilities = new ArrayList<DesiredCapabilities>(); + for (Browser browser : browsers) { + capabilities.add(browser.getDesiredCapabilities()); + } + return capabilities; + } +} diff --git a/uitest-common/src/main/java/com/vaadin/tests/tb3/ParallelScheduler.java b/uitest-common/src/main/java/com/vaadin/tests/tb3/ParallelScheduler.java new file mode 100644 index 0000000000..ef9ee382d0 --- /dev/null +++ b/uitest-common/src/main/java/com/vaadin/tests/tb3/ParallelScheduler.java @@ -0,0 +1,69 @@ +/* + * 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.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + +import org.junit.runners.model.RunnerScheduler; + +/** + * JUnit scheduler capable of running multiple tets in parallel. Each test is + * run in its own thread. Uses an {@link ExecutorService} to manage the threads. + * + * @author Vaadin Ltd + */ +public class ParallelScheduler implements RunnerScheduler { + private final List<Future<Object>> fResults = new ArrayList<Future<Object>>(); + private ExecutorService fService; + + /** + * Creates a parallel scheduler which will use the given executor service + * when submitting test jobs. + * + * @param service + * The service to use for tests + */ + public ParallelScheduler(ExecutorService service) { + fService = service; + } + + @Override + public void schedule(final Runnable childStatement) { + fResults.add(fService.submit(new Callable<Object>() { + @Override + public Object call() throws Exception { + childStatement.run(); + return null; + } + })); + } + + @Override + public void finished() { + for (Future<Object> each : fResults) { + try { + each.get(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } +} diff --git a/uitest-common/src/main/java/com/vaadin/tests/tb3/ParameterizedTB3Runner.java b/uitest-common/src/main/java/com/vaadin/tests/tb3/ParameterizedTB3Runner.java new file mode 100644 index 0000000000..06e1ea9df9 --- /dev/null +++ b/uitest-common/src/main/java/com/vaadin/tests/tb3/ParameterizedTB3Runner.java @@ -0,0 +1,168 @@ +/* + * 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.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.runners.Parameterized.Parameters; +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.InitializationError; + +/** + * TestBench test runner which supports static @Parameters annotated methods + * providing parameters for the corresponding setter. + * <p> + * {@code @Parameters public static Collection<String> getThemes() } creates one + * permutation for each value returned by {@code getThemes()}. The value is + * automatically assigned to the test instance using {@code setTheme(String)} + * before invoking the test method + * + * @author Vaadin Ltd + */ +public class ParameterizedTB3Runner extends TB3Runner { + + public ParameterizedTB3Runner(Class<?> klass) throws InitializationError { + super(klass); + } + + @Override + protected List<FrameworkMethod> computeTestMethods() { + List<FrameworkMethod> methods = super.computeTestMethods(); + + Map<Method, Collection<String>> parameters = new LinkedHashMap<Method, Collection<String>>(); + + // Find all @Parameters methods and invoke them to find out permutations + + for (Method m : getTestClass().getJavaClass().getMethods()) { + Parameters p = m.getAnnotation(Parameters.class); + if (p == null) { + continue; + } + + if (!m.getName().startsWith("get") || !m.getName().endsWith("s")) { + throw new IllegalStateException( + "Method " + + m.getName() + + " is annotated with @Parameter but is not named getSomeThings() as it should"); + } + + if (m.getParameterTypes().length != 0) { + throw new IllegalStateException( + "Method " + + m.getName() + + " annotated with @Parameter should not have any arguments"); + } + + if (!Modifier.isStatic(m.getModifiers())) { + throw new IllegalStateException("Method " + m.getName() + + " annotated with @Parameter must be static"); + } + + // getThemes -> setTheme + String setter = "set" + m.getName().substring("get".length()); + setter = setter.substring(0, setter.length() - 1); + // property = property.substring(0, 1).toLowerCase() + // + property.substring(1); + + Method setterMethod; + try { + setterMethod = getTestClass().getJavaClass().getMethod(setter, + String.class); + } catch (Exception e) { + throw new IllegalStateException("No setter " + setter + + " found in " + + getTestClass().getJavaClass().getName(), e); + } + + Collection<String> values; + try { + values = (Collection<String>) m.invoke(null); + if (!values.isEmpty()) { + // Ignore any empty collections to allow e.g. integration + // tests to use "/demo" path by default without adding that + // to the screenshot name + parameters.put(setterMethod, values); + } + } catch (Exception e) { + throw new IllegalStateException("The setter " + m.getName() + + " could not be invoked", e); + } + } + + // Add method permutations for all @Parameters + for (Method setter : parameters.keySet()) { + List<FrameworkMethod> newMethods = new ArrayList<FrameworkMethod>(); + for (FrameworkMethod m : methods) { + + if (!(m instanceof TBMethod)) { + System.err.println("Unknown method type: " + + m.getClass().getName()); + newMethods.add(m); + continue; + } + + // testFoo + // testBar + // -> + // testFoo[valo] + // testFoo[runo] + // testBar[valo] + // testBar[runo] + + for (final String value : parameters.get(setter)) { + newMethods.add(new TBMethodWithBefore((TBMethod) m, setter, + value)); + } + } + // Update methods so next parameters will use all expanded methods + methods = newMethods; + } + return methods; + } + + public static class TBMethodWithBefore extends TBMethod { + + private Method setter; + private String value; + private TBMethod parent; + + public TBMethodWithBefore(TBMethod m, Method setter, String value) { + super(m.getMethod(), m.getCapabilities()); + parent = m; + this.setter = setter; + this.value = value; + } + + @Override + public Object invokeExplosively(Object target, Object... params) + throws Throwable { + setter.invoke(target, value); + return parent.invokeExplosively(target, params); + } + + @Override + public String getName() { + return parent.getName() + "[" + value + "]"; + }; + + } +} diff --git a/uitest-common/src/main/java/com/vaadin/tests/tb3/PrivateTB3Configuration.java b/uitest-common/src/main/java/com/vaadin/tests/tb3/PrivateTB3Configuration.java new file mode 100644 index 0000000000..85104c784b --- /dev/null +++ b/uitest-common/src/main/java/com/vaadin/tests/tb3/PrivateTB3Configuration.java @@ -0,0 +1,281 @@ +/* + * Copyright 2000-2013 Vaadind 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.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Calendar; +import java.util.Enumeration; +import java.util.Properties; + +import org.junit.Assert; +import org.openqa.selenium.ie.InternetExplorerDriver; +import org.openqa.selenium.remote.DesiredCapabilities; + +import com.vaadin.testbench.annotations.BrowserFactory; +import com.vaadin.testbench.annotations.RunLocally; +import com.vaadin.testbench.annotations.RunOnHub; +import com.vaadin.testbench.parallel.Browser; +import com.vaadin.testbench.parallel.BrowserUtil; + +/** + * Provides values for parameters which depend on where the test is run. + * Parameters should be configured in work/eclipse-run-selected-test.properties. + * A template is available in uitest/. + * + * @author Vaadin Ltd + */ +@RunOnHub("tb3-hub.intra.itmill.com") +@BrowserFactory(VaadinBrowserFactory.class) +public abstract class PrivateTB3Configuration extends ScreenshotTB3Test { + /** + * + */ + public static final String SCREENSHOT_DIRECTORY = "com.vaadin.testbench.screenshot.directory"; + private static final String HOSTNAME_PROPERTY = "com.vaadin.testbench.deployment.hostname"; + private static final String RUN_LOCALLY_PROPERTY = "com.vaadin.testbench.runLocally"; + private static final String ALLOW_RUN_LOCALLY_PROPERTY = "com.vaadin.testbench.allowRunLocally"; + private static final String PORT_PROPERTY = "com.vaadin.testbench.deployment.port"; + private static final String DEPLOYMENT_PROPERTY = "com.vaadin.testbench.deployment.url"; + private static final String HUB_URL = "com.vaadin.testbench.hub.url"; + private static final Properties properties = new Properties(); + private static final File propertiesFile = new File("../work", + "eclipse-run-selected-test.properties"); + private static final String FIREFOX_PATH = "firefox.path"; + private static final String PHANTOMJS_PATH = "phantomjs.binary.path"; + + static { + if (propertiesFile.exists()) { + try { + properties.load(new FileInputStream(propertiesFile)); + if (properties.containsKey(RUN_LOCALLY_PROPERTY)) { + System.setProperty("useLocalWebDriver", "true"); + DesiredCapabilities localBrowser = getRunLocallyCapabilities(); + System.setProperty( + "browsers.include", + localBrowser.getBrowserName() + + localBrowser.getVersion()); + } + if (properties.containsKey(FIREFOX_PATH)) { + System.setProperty(FIREFOX_PATH, + properties.getProperty(FIREFOX_PATH)); + } + if (properties.containsKey(PHANTOMJS_PATH)) { + System.setProperty(PHANTOMJS_PATH, + properties.getProperty(PHANTOMJS_PATH)); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + @Override + public void setup() throws Exception { + String allowRunLocally = getProperty(ALLOW_RUN_LOCALLY_PROPERTY); + if ((allowRunLocally == null || !allowRunLocally.equals("" + true)) + && getClass().getAnnotation(RunLocally.class) != null) { + Assert.fail("@RunLocally annotation is not allowed by default in framework tests. " + + "See file uitest/eclipse-run-selected-test.properties for more information."); + } + + super.setup(); + } + + @Override + public void setDesiredCapabilities(DesiredCapabilities desiredCapabilities) { + super.setDesiredCapabilities(desiredCapabilities); + + if (BrowserUtil.isIE(desiredCapabilities)) { + if (requireWindowFocusForIE()) { + desiredCapabilities.setCapability( + InternetExplorerDriver.REQUIRE_WINDOW_FOCUS, true); + } + if (!usePersistentHoverForIE()) { + desiredCapabilities.setCapability( + InternetExplorerDriver.ENABLE_PERSISTENT_HOVERING, + false); + } + if (!useNativeEventsForIE()) { + desiredCapabilities.setCapability( + InternetExplorerDriver.NATIVE_EVENTS, false); + } + } + + desiredCapabilities.setCapability("project", "Vaadin Framework"); + desiredCapabilities.setCapability("build", String.format("%s / %s", + getDeploymentHostname(), Calendar.getInstance().getTime())); + desiredCapabilities.setCapability( + "name", + String.format("%s.%s", getClass().getCanonicalName(), + testName.getMethodName())); + } + + protected static DesiredCapabilities getRunLocallyCapabilities() { + VaadinBrowserFactory factory = new VaadinBrowserFactory(); + try { + return factory.create(Browser.valueOf(properties.getProperty( + RUN_LOCALLY_PROPERTY).toUpperCase())); + } catch (Exception e) { + System.err.println(e.getMessage()); + System.err.println("Falling back to FireFox"); + } + return factory.create(Browser.FIREFOX); + } + + protected static String getProperty(String name) { + String property = properties.getProperty(name); + if (property == null) { + property = System.getProperty(name); + } + + return property; + } + + @Override + protected String getScreenshotDirectory() { + String screenshotDirectory = getProperty(SCREENSHOT_DIRECTORY); + if (screenshotDirectory == null) { + throw new RuntimeException( + "No screenshot directory defined. Use -D" + + SCREENSHOT_DIRECTORY + "=<path>"); + } + return screenshotDirectory; + } + + @Override + protected String getHubURL() { + String hubUrl = getProperty(HUB_URL); + if (hubUrl == null || hubUrl.trim().isEmpty()) { + return super.getHubURL(); + } + + return hubUrl; + } + + @Override + protected String getBaseURL() { + if (isRunLocally()) { + return "http://localhost:8888"; + } + String url = getProperty(DEPLOYMENT_PROPERTY); + if (url == null || url.trim().isEmpty()) { + return super.getBaseURL(); + } + return url; + } + + @Override + protected String getDeploymentHostname() { + if (isRunLocally()) { + return "localhost"; + } + return getConfiguredDeploymentHostname(); + } + + protected boolean isRunLocally() { + if (properties.containsKey(RUN_LOCALLY_PROPERTY)) { + return true; + } + + if (properties.containsKey(ALLOW_RUN_LOCALLY_PROPERTY) + && properties.get(ALLOW_RUN_LOCALLY_PROPERTY).equals("true") + && getClass().getAnnotation(RunLocally.class) != null) { + return true; + } + + return false; + } + + /** + * Gets the hostname that tests are configured to use. + * + * @return the host name configuration value + */ + public static String getConfiguredDeploymentHostname() { + String hostName = getProperty(HOSTNAME_PROPERTY); + + if (hostName == null || "".equals(hostName)) { + hostName = findAutoHostname(); + } + + return hostName; + } + + @Override + protected int getDeploymentPort() { + return getConfiguredDeploymentPort(); + } + + /** + * Gets the port that tests are configured to use. + * + * @return the port configuration value + */ + public static int getConfiguredDeploymentPort() { + String portString = getProperty(PORT_PROPERTY); + + int port = 8888; + if (portString != null && !"".equals(portString)) { + port = Integer.parseInt(portString); + } + + return port; + } + + /** + * Tries to automatically determine the IP address of the machine the test + * is running on. + * + * @return An IP address of one of the network interfaces in the machine. + * @throws RuntimeException + * if there was an error or no IP was found + */ + private static String findAutoHostname() { + try { + Enumeration<NetworkInterface> interfaces = NetworkInterface + .getNetworkInterfaces(); + while (interfaces.hasMoreElements()) { + NetworkInterface nwInterface = interfaces.nextElement(); + if (!nwInterface.isUp() || nwInterface.isLoopback() + || nwInterface.isVirtual()) { + continue; + } + Enumeration<InetAddress> addresses = nwInterface + .getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress address = addresses.nextElement(); + if (address.isLoopbackAddress()) { + continue; + } + if (address.isSiteLocalAddress()) { + return address.getHostAddress(); + } + } + } + } catch (SocketException e) { + throw new RuntimeException("Could not enumerate "); + } + + throw new RuntimeException( + "No compatible (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) ip address found."); + } +} diff --git a/uitest-common/src/main/java/com/vaadin/tests/tb3/RetryOnFail.java b/uitest-common/src/main/java/com/vaadin/tests/tb3/RetryOnFail.java new file mode 100644 index 0000000000..7a1656a0d2 --- /dev/null +++ b/uitest-common/src/main/java/com/vaadin/tests/tb3/RetryOnFail.java @@ -0,0 +1,64 @@ +/* + * 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 org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +public class RetryOnFail implements TestRule { + @Override + public Statement apply(Statement base, Description description) { + return statement(base, description); + } + + private Statement statement(final Statement base, + final Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + Throwable caughtThrowable = null; + int retryCount = getRetryCount(); + + for (int i = 0; i <= retryCount; i++) { + try { + base.evaluate(); + return; + } catch (Throwable t) { + caughtThrowable = t; + System.err.println(String.format( + "%s: run %s/%s failed.", + description.getDisplayName(), i + 1, + retryCount + 1)); + System.err.println(t.getMessage()); + } + } + throw caughtThrowable; + } + + private int getRetryCount() { + String retryCount = System + .getProperty("com.vaadin.testbench.max.retries"); + + if (retryCount != null && retryCount != "") { + return Integer.parseInt(retryCount); + } + + return 0; + } + }; + } +} diff --git a/uitest-common/src/main/java/com/vaadin/tests/tb3/SauceLabsBrowserFactory.java b/uitest-common/src/main/java/com/vaadin/tests/tb3/SauceLabsBrowserFactory.java new file mode 100644 index 0000000000..8eae64d0b8 --- /dev/null +++ b/uitest-common/src/main/java/com/vaadin/tests/tb3/SauceLabsBrowserFactory.java @@ -0,0 +1,117 @@ +/* + * 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 org.openqa.selenium.Platform; +import org.openqa.selenium.ie.InternetExplorerDriver; +import org.openqa.selenium.remote.DesiredCapabilities; + +import com.vaadin.shared.Version; +import com.vaadin.testbench.parallel.Browser; +import com.vaadin.testbench.parallel.DefaultBrowserFactory; + +public class SauceLabsBrowserFactory extends DefaultBrowserFactory { + + @Override + public DesiredCapabilities create(Browser browser) { + String version = ""; + // need to pick a version to request, but for these two auto-updating + // browsers there is a special value "latest" (and "latest-1", + // "latest-2") + if (Browser.FIREFOX.equals(browser) || Browser.CHROME.equals(browser)) { + version = "latest"; + } + return create(browser, version, Platform.ANY); + } + + @Override + public DesiredCapabilities create(Browser browser, String version, + Platform platform) { + DesiredCapabilities caps; + + switch (browser) { + case CHROME: + caps = DesiredCapabilities.chrome(); + caps.setVersion(version); + break; + case PHANTOMJS: + // This will not work on SauceLabs - should be filtered with + // browsers.exclude. However, we cannot throw an exception here as + // filtering only takes place if there is no exception. + caps = DesiredCapabilities.phantomjs(); + caps.setVersion("1"); + caps.setPlatform(Platform.LINUX); + break; + case SAFARI: + caps = DesiredCapabilities.safari(); + caps.setVersion(version); + break; + case IE8: + caps = DesiredCapabilities.internetExplorer(); + caps.setVersion("8.0"); + caps.setCapability(InternetExplorerDriver.IE_ENSURE_CLEAN_SESSION, + true); + break; + case IE9: + caps = DesiredCapabilities.internetExplorer(); + caps.setVersion("9.0"); + caps.setCapability(InternetExplorerDriver.IE_ENSURE_CLEAN_SESSION, + true); + break; + case IE10: + caps = DesiredCapabilities.internetExplorer(); + caps.setVersion("10.0"); + caps.setCapability(InternetExplorerDriver.IE_ENSURE_CLEAN_SESSION, + true); + break; + case IE11: + caps = DesiredCapabilities.internetExplorer(); + caps.setVersion("11.0"); + // There are 2 capabilities ie.ensureCleanSession and + // ensureCleanSession in Selenium + // IE 11 uses ie.ensureCleanSession + caps.setCapability("ie.ensureCleanSession", true); + caps.setCapability(InternetExplorerDriver.IE_ENSURE_CLEAN_SESSION, + true); + break; + case FIREFOX: + caps = DesiredCapabilities.firefox(); + caps.setVersion(version); + break; + default: + caps = DesiredCapabilities.firefox(); + caps.setVersion(version); + caps.setPlatform(platform); + } + + if (!Browser.PHANTOMJS.equals(browser)) { + caps.setCapability("platform", "Windows 7"); + } + + // accept self-signed certificates + caps.setCapability("acceptSslCerts", "true"); + + // SauceLabs specific parts + + caps.setCapability("screenResolution", "1680x1050"); + + // build and project for easy identification in SauceLabs UI + caps.setCapability("build", Version.getFullVersion()); + + return caps; + } + +} diff --git a/uitest-common/src/main/java/com/vaadin/tests/tb3/ScreenshotTB3Test.java b/uitest-common/src/main/java/com/vaadin/tests/tb3/ScreenshotTB3Test.java new file mode 100644 index 0000000000..d94e05efdb --- /dev/null +++ b/uitest-common/src/main/java/com/vaadin/tests/tb3/ScreenshotTB3Test.java @@ -0,0 +1,498 @@ +/* + * 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.io.File; +import java.io.FileFilter; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.io.FileUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.rules.TestRule; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.remote.DesiredCapabilities; + +import com.vaadin.testbench.Parameters; +import com.vaadin.testbench.ScreenshotOnFailureRule; +import com.vaadin.testbench.parallel.BrowserUtil; +import com.vaadin.testbench.screenshot.ImageFileUtil; + +/** + * Base class which provides functionality for tests which use the automatic + * screenshot comparison function. + * + * @author Vaadin Ltd + */ +public abstract class ScreenshotTB3Test extends AbstractTB3Test { + + @Rule + public ScreenshotOnFailureRule screenshotOnFailure = new ScreenshotOnFailureRule( + this, true) { + + @Override + protected void failed(Throwable throwable, Description description) { + super.failed(throwable, description); + closeApplication(); + } + + @Override + protected void succeeded(Description description) { + super.succeeded(description); + closeApplication(); + } + + @Override + protected File getErrorScreenshotFile(Description description) { + return ImageFileUtil + .getErrorScreenshotFile(getScreenshotFailureName()); + }; + }; + + private String screenshotBaseName; + + @Rule + public TestRule watcher = new TestWatcher() { + + @Override + protected void starting(org.junit.runner.Description description) { + Class<?> testClass = description.getTestClass(); + // Runner adds [BrowserName] which we do not want to use in the + // screenshot name + String testMethod = description.getMethodName(); + testMethod = testMethod.replaceAll("\\[.*\\]", ""); + + String className = testClass.getSimpleName(); + screenshotBaseName = className + "-" + testMethod; + } + }; + + /** + * Contains a list of screenshot identifiers for which + * {@link #compareScreen(String)} has failed during the test + */ + private List<String> screenshotFailures; + + /** + * Defines TestBench screen comparison parameters before each test run + */ + @Before + public void setupScreenComparisonParameters() { + screenshotFailures = new ArrayList<String>(); + + Parameters.setScreenshotErrorDirectory(getScreenshotErrorDirectory()); + Parameters + .setScreenshotReferenceDirectory(getScreenshotReferenceDirectory()); + } + + /** + * Grabs a screenshot and compares with the reference image with the given + * identifier. Supports alternative references and will succeed if the + * screenshot matches at least one of the references. + * + * In case of a failed comparison this method stores the grabbed screenshots + * in the error directory as defined by + * {@link #getScreenshotErrorDirectory()}. It will also generate a html file + * in the same directory, comparing the screenshot with the first found + * reference. + * + * @param identifier + * @throws IOException + */ + protected void compareScreen(String identifier) throws IOException { + compareScreen(null, identifier); + } + + protected void compareScreen(WebElement element, String identifier) + throws IOException { + if (identifier == null || identifier.isEmpty()) { + throw new IllegalArgumentException("Empty identifier not supported"); + } + + File mainReference = getScreenshotReferenceFile(identifier); + + List<File> referenceFiles = findReferenceAndAlternatives(mainReference); + List<File> failedReferenceFiles = new ArrayList<File>(); + + for (File referenceFile : referenceFiles) { + boolean match = false; + if (element == null) { + // Full screen + match = testBench(driver).compareScreen(referenceFile); + } else { + // Only the element + match = customTestBench(driver).compareScreen(element, + referenceFile, + BrowserUtil.isIE8(getDesiredCapabilities())); + } + if (match) { + // There might be failure files because of retries in TestBench. + deleteFailureFiles(getErrorFileFromReference(referenceFile)); + break; + } else { + failedReferenceFiles.add(referenceFile); + } + } + + File referenceToKeep = null; + if (failedReferenceFiles.size() == referenceFiles.size()) { + // Ensure we use the correct browser version (e.g. if running IE11 + // and only an IE 10 reference was available, then mainReference + // will be for IE 10, not 11) + String originalName = getScreenshotReferenceName(identifier); + File exactVersionFile = new File(originalName); + + if (!exactVersionFile.equals(mainReference)) { + // Rename png+html to have the correct version + File correctPng = getErrorFileFromReference(exactVersionFile); + File producedPng = getErrorFileFromReference(mainReference); + File correctHtml = htmlFromPng(correctPng); + File producedHtml = htmlFromPng(producedPng); + + producedPng.renameTo(correctPng); + producedHtml.renameTo(correctHtml); + referenceToKeep = exactVersionFile; + screenshotFailures.add(exactVersionFile.getName()); + } else { + // All comparisons failed, keep the main error image + HTML + screenshotFailures.add(mainReference.getName()); + referenceToKeep = mainReference; + } + } + + // Remove all PNG/HTML files we no longer need (failed alternative + // references or all error files (PNG/HTML) if comparison succeeded) + for (File failedAlternative : failedReferenceFiles) { + File failurePng = getErrorFileFromReference(failedAlternative); + if (failedAlternative != referenceToKeep) { + // Delete png + HTML + deleteFailureFiles(failurePng); + } + } + if (referenceToKeep != null) { + File errorPng = getErrorFileFromReference(referenceToKeep); + enableAutoswitch(new File(errorPng.getParentFile(), + errorPng.getName() + ".html")); + } + } + + private CustomTestBenchCommandExecutor customTestBench = null; + + private CustomTestBenchCommandExecutor customTestBench(WebDriver driver) { + if (customTestBench == null) { + customTestBench = new CustomTestBenchCommandExecutor(driver); + } + + return customTestBench; + } + + private void enableAutoswitch(File htmlFile) throws FileNotFoundException, + IOException { + if (htmlFile == null || !htmlFile.exists()) { + return; + } + + String html = FileUtils.readFileToString(htmlFile); + + html = html.replace("body onclick=\"", + "body onclick=\"clearInterval(autoSwitch);"); + html = html.replace("</script>", + ";autoSwitch=setInterval(switchImage,500);</script>"); + + FileUtils.writeStringToFile(htmlFile, html); + } + + private void deleteFailureFiles(File failurePng) { + File failureHtml = htmlFromPng(failurePng); + + failurePng.delete(); + failureHtml.delete(); + } + + /** + * Returns a new File which points to a .html file instead of the given .png + * file + * + * @param png + * @return + */ + private static File htmlFromPng(File png) { + return new File(png.getParentFile(), png.getName().replaceAll( + "\\.png$", ".png.html")); + } + + /** + * + * @param referenceFile + * The reference image file (in the directory defined by + * {@link #getScreenshotReferenceDirectory()}) + * @return the file name of the file generated in the directory defined by + * {@link #getScreenshotErrorDirectory()} if comparison with the + * given reference image fails. + */ + private File getErrorFileFromReference(File referenceFile) { + + String absolutePath = referenceFile.getAbsolutePath(); + String screenshotReferenceDirectory = getScreenshotReferenceDirectory(); + String screenshotErrorDirectory = getScreenshotErrorDirectory(); + // We throw an exception to safeguard against accidental reference + // deletion. See (#14446) + if (!absolutePath.contains(screenshotReferenceDirectory)) { + throw new IllegalStateException( + "Reference screenshot not in reference directory. Screenshot path: '" + + absolutePath + "', directory path: '" + + screenshotReferenceDirectory + "'"); + } + return new File(absolutePath.replace(screenshotReferenceDirectory, + screenshotErrorDirectory)); + } + + /** + * Finds alternative references for the given files + * + * @param reference + * @return all references which should be considered when comparing with the + * given files, including the given reference + */ + private List<File> findReferenceAndAlternatives(File reference) { + List<File> files = new ArrayList<File>(); + files.add(reference); + + File screenshotDir = reference.getParentFile(); + String name = reference.getName(); + // Remove ".png" + String nameBase = name.substring(0, name.length() - 4); + for (int i = 1;; i++) { + File file = new File(screenshotDir, nameBase + "_" + i + ".png"); + if (file.exists()) { + files.add(file); + } else { + break; + } + } + + return files; + } + + /** + * @param testName + * @return the reference file name to use for the given browser, as + * described by {@literal capabilities}, and identifier + */ + private File getScreenshotReferenceFile(String identifier) { + DesiredCapabilities capabilities = getDesiredCapabilities(); + + String originalName = getScreenshotReferenceName(identifier); + File exactVersionFile = new File(originalName); + if (exactVersionFile.exists()) { + return exactVersionFile; + } + + String browserVersion = capabilities.getVersion(); + + // compare against screenshots for this version and older + // default such that if no particular version is requested, compare with + // any version + int maxVersion = 100; + if (browserVersion.matches("\\d+")) { + maxVersion = Integer.parseInt(browserVersion); + } + for (int version = maxVersion; version > 0; version--) { + String fileName = getScreenshotReferenceName(identifier, version); + File oldVersionFile = new File(fileName); + if (oldVersionFile.exists()) { + return oldVersionFile; + } + } + + return exactVersionFile; + } + + /** + * @return the base directory of 'reference' and 'errors' screenshots + */ + protected abstract String getScreenshotDirectory(); + + /** + * @return the base directory of 'reference' and 'errors' screenshots with a + * trailing file separator + */ + private String getScreenshotDirectoryWithTrailingSeparator() { + String screenshotDirectory = getScreenshotDirectory(); + if (!screenshotDirectory.endsWith(File.separator)) { + screenshotDirectory += File.separator; + } + return screenshotDirectory; + } + + /** + * @return the directory where reference images are stored (the 'reference' + * folder inside the screenshot directory) + */ + private String getScreenshotReferenceDirectory() { + return getScreenshotDirectoryWithTrailingSeparator() + "reference"; + } + + /** + * @return the directory where comparison error images should be created + * (the 'errors' folder inside the screenshot directory) + */ + private String getScreenshotErrorDirectory() { + return getScreenshotDirectoryWithTrailingSeparator() + "errors"; + } + + /** + * Checks if any screenshot comparisons failures occurred during the test + * and combines all comparison errors into one exception + * + * @throws IOException + * If there were failures during the test + */ + @After + public void checkCompareFailures() throws IOException { + if (screenshotFailures != null && !screenshotFailures.isEmpty()) { + throw new IOException( + "The following screenshots did not match the reference: " + + screenshotFailures.toString()); + } + + } + + /** + * @return the name of a "failure" image which is stored in the folder + * defined by {@link #getScreenshotErrorDirectory()} when the test + * fails + */ + private String getScreenshotFailureName() { + return getScreenshotBaseName() + "_" + getUniqueIdentifier(null) + + "-failure.png"; + } + + /** + * @return the base name used for screenshots. This is the first part of the + * screenshot file name, typically created as "testclass-testmethod" + */ + public String getScreenshotBaseName() { + return screenshotBaseName; + } + + /** + * Returns the name of the reference file based on the given parameters. + * + * @param testName + * @param capabilities + * @param identifier + * @return the full path of the reference + */ + private String getScreenshotReferenceName(String identifier) { + return getScreenshotReferenceName(identifier, null); + } + + /** + * Returns the name of the reference file based on the given parameters. The + * version given in {@literal capabilities} is used unless it is overridden + * by the {@literal versionOverride} parameter. + * + * @param testName + * @param capabilities + * @param identifier + * @return the full path of the reference + */ + private String getScreenshotReferenceName(String identifier, + Integer versionOverride) { + return getScreenshotReferenceDirectory() + File.separator + + getScreenshotBaseName() + "_" + + getUniqueIdentifier(versionOverride) + "_" + identifier + + ".png"; + } + + private String getUniqueIdentifier(Integer versionOverride) { + String testNameAndParameters = testName.getMethodName(); + // runTest-wildfly9-nginx[Windows_Firefox_24][/buffering/demo][valo] + + String parameters = testNameAndParameters.substring( + testNameAndParameters.indexOf("[") + 1, + testNameAndParameters.length() - 1); + // Windows_Firefox_24][/buffering/demo][valo + + parameters = parameters.replace("][", "_"); + // Windows_Firefox_24_/buffering/demo_valo + + parameters = parameters.replace("/", ""); + // Windows_Firefox_24_bufferingdemo_valo + + if (versionOverride != null) { + // Windows_Firefox_17_bufferingdemo_valo + parameters = parameters.replaceFirst("_" + + getDesiredCapabilities().getVersion(), "_" + + versionOverride); + } + return parameters; + } + + /** + * Returns the base name of the screenshot in the error directory. This is a + * name so that all files matching {@link #getScreenshotErrorBaseName()}* + * are owned by this test instance (taking into account + * {@link #getDesiredCapabilities()}) and can safely be removed before + * running this test. + */ + private String getScreenshotErrorBaseName() { + return getScreenshotReferenceName("dummy", null).replace( + getScreenshotReferenceDirectory(), + getScreenshotErrorDirectory()).replace("_dummy.png", ""); + } + + /** + * Removes any old screenshots related to this test from the errors + * directory before running the test + */ + @Before + public void cleanErrorDirectory() { + // Remove any screenshots for this test from the error directory + // before running it. Leave unrelated files as-is + File errorDirectory = new File(getScreenshotErrorDirectory()); + + // Create errors directory if it does not exist + if (!errorDirectory.exists()) { + errorDirectory.mkdirs(); + } + + final String errorBase = getScreenshotErrorBaseName(); + File[] files = errorDirectory.listFiles(new FileFilter() { + + @Override + public boolean accept(File pathname) { + String thisFile = pathname.getAbsolutePath(); + if (thisFile.startsWith(errorBase)) { + return true; + } + return false; + } + }); + for (File f : files) { + f.delete(); + } + } +} diff --git a/uitest-common/src/main/java/com/vaadin/tests/tb3/SingleBrowserTest.java b/uitest-common/src/main/java/com/vaadin/tests/tb3/SingleBrowserTest.java new file mode 100644 index 0000000000..b5389d3eec --- /dev/null +++ b/uitest-common/src/main/java/com/vaadin/tests/tb3/SingleBrowserTest.java @@ -0,0 +1,34 @@ +/* + * 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.util.Collections; +import java.util.List; + +import org.openqa.selenium.remote.DesiredCapabilities; + +import com.vaadin.testbench.parallel.Browser; + +public abstract class SingleBrowserTest extends PrivateTB3Configuration { + @Override + public List<DesiredCapabilities> getBrowsersToTest() { + if (isRunLocally()) { + return Collections.singletonList(getRunLocallyCapabilities()); + } + return Collections.singletonList(Browser.PHANTOMJS + .getDesiredCapabilities()); + } +} diff --git a/uitest-common/src/main/java/com/vaadin/tests/tb3/SingleBrowserTestPhantomJS2.java b/uitest-common/src/main/java/com/vaadin/tests/tb3/SingleBrowserTestPhantomJS2.java new file mode 100644 index 0000000000..7447a3c056 --- /dev/null +++ b/uitest-common/src/main/java/com/vaadin/tests/tb3/SingleBrowserTestPhantomJS2.java @@ -0,0 +1,29 @@ +/* + * 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.util.Collections; +import java.util.List; + +import org.openqa.selenium.remote.DesiredCapabilities; + +public abstract class SingleBrowserTestPhantomJS2 extends + PrivateTB3Configuration { + @Override + public List<DesiredCapabilities> getBrowsersToTest() { + return Collections.singletonList(PHANTOMJS2()); + } +} diff --git a/uitest-common/src/main/java/com/vaadin/tests/tb3/TB3Runner.java b/uitest-common/src/main/java/com/vaadin/tests/tb3/TB3Runner.java new file mode 100644 index 0000000000..4917e398cf --- /dev/null +++ b/uitest-common/src/main/java/com/vaadin/tests/tb3/TB3Runner.java @@ -0,0 +1,80 @@ +/* + * 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.reflect.Field; +import java.lang.reflect.Modifier; + +import org.apache.http.client.HttpClient; +import org.junit.runners.Parameterized; +import org.junit.runners.model.InitializationError; +import org.openqa.selenium.remote.internal.ApacheHttpClient; +import org.openqa.selenium.remote.internal.HttpClientFactory; + +import com.vaadin.testbench.parallel.ParallelRunner; + +/** + * This runner is loosely based on FactoryTestRunner by Ted Young + * (http://tedyoung.me/2011/01/23/junit-runtime-tests-custom-runners/). The + * generated test names give information about the parameters used (unlike + * {@link Parameterized}). + * + * @since 7.1 + */ +public class TB3Runner extends ParallelRunner { + + /** + * Socket timeout for HTTP connections to the grid hub. The connection is + * closed after 30 minutes of inactivity to avoid builds hanging for up to + * three hours per connection if the test client crashes/hangs. + */ + private static final int SOCKET_TIMEOUT = 30 * 60 * 1000; + + static { + + // reduce socket timeout to avoid tests hanging for three hours + try { + Field field = ApacheHttpClient.Factory.class + .getDeclaredField("defaultClientFactory"); + assert (Modifier.isStatic(field.getModifiers())); + field.setAccessible(true); + field.set(null, new HttpClientFactory() { + @Override + public HttpClient getGridHttpClient(int connection_timeout, + int socket_timeout) { + + if (socket_timeout == 0 || socket_timeout > SOCKET_TIMEOUT) { + return super.getGridHttpClient(connection_timeout, + SOCKET_TIMEOUT); + } + + return super.getGridHttpClient(connection_timeout, + socket_timeout); + } + }); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException( + "Changing socket timeout for TestBench failed", e); + } + } + + public TB3Runner(Class<?> klass) throws InitializationError { + super(klass); + } + +} diff --git a/uitest-common/src/main/java/com/vaadin/tests/tb3/TB3TestLocator.java b/uitest-common/src/main/java/com/vaadin/tests/tb3/TB3TestLocator.java new file mode 100644 index 0000000000..eb0861757e --- /dev/null +++ b/uitest-common/src/main/java/com/vaadin/tests/tb3/TB3TestLocator.java @@ -0,0 +1,229 @@ +/* + * 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.io.File; +import java.io.IOException; +import java.lang.reflect.Modifier; +import java.net.JarURLConnection; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.JarEntry; + +public class TB3TestLocator { + /** + * Traverses the directory on the classpath (inside or outside a Jar file) + * specified by 'basePackage'. Collects all classes inside the location + * which can be assigned to 'baseClass' except for classes inside packages + * listed in 'ignoredPackages'. + * + * @param baseClass + * @param basePackage + * @param ignorePackages + * @return + */ + public Class<?>[] findTests(Class<? extends AbstractTB3Test> baseClass, + String basePackage, String[] ignorePackages) { + try { + List<?> l = findClasses(baseClass, basePackage, ignorePackages); + return l.toArray(new Class[] {}); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return null; + } + + /** + * Traverses the directory on the classpath (inside or outside a Jar file) + * specified by 'basePackage'. Collects all classes inside the location + * which can be assigned to 'baseClass' except for classes inside packages + * listed in 'ignoredPackages'. + * + * @param baseClass + * @param basePackage + * @param ignoredPackages + * @return + * @throws IOException + */ + protected <T> List<Class<? extends T>> findClasses(Class<T> baseClass, + String basePackage, String[] ignoredPackages) throws IOException { + List<Class<? extends T>> classes = new ArrayList<Class<? extends T>>(); + String basePackageDirName = "/" + basePackage.replace('.', '/'); + URL location = baseClass.getResource(basePackageDirName); + if (location.getProtocol().equals("file")) { + try { + File f = new File(location.toURI()); + if (!f.exists()) { + throw new IOException("Directory " + f.toString() + + " does not exist"); + } + findPackages(f, basePackage, baseClass, classes, + ignoredPackages); + } catch (URISyntaxException e) { + throw new IOException(e.getMessage()); + } + } else if (location.getProtocol().equals("jar")) { + JarURLConnection juc = (JarURLConnection) location.openConnection(); + findClassesInJar(juc, basePackage, baseClass, classes); + } + + Collections.sort(classes, new Comparator<Class<? extends T>>() { + + @Override + public int compare(Class<? extends T> o1, Class<? extends T> o2) { + return o1.getName().compareTo(o2.getName()); + } + + }); + + return classes; + } + + /** + * Traverses the given directory and collects all classes which are inside + * the given 'javaPackage' and can be assigned to the given 'baseClass'. The + * found classes are added to 'result'. + * + * @param parent + * The directory to traverse + * @param javaPackage + * The java package which 'parent' contains + * @param baseClass + * The class which the target classes extend + * @param result + * The collection to which found classes are added + * @param ignoredPackages + * A collection of packages (including sub packages) to ignore + */ + private <T> void findPackages(File parent, String javaPackage, + Class<T> baseClass, Collection<Class<? extends T>> result, + String[] ignoredPackages) { + for (String ignoredPackage : ignoredPackages) { + if (javaPackage.equals(ignoredPackage)) { + return; + } + } + + for (File file : parent.listFiles()) { + if (file.isDirectory()) { + findPackages(file, javaPackage + "." + file.getName(), + baseClass, result, ignoredPackages); + } else if (file.getName().endsWith(".class")) { + String fullyQualifiedClassName = javaPackage + "." + + file.getName().replace(".class", ""); + addClassIfMatches(result, fullyQualifiedClassName, baseClass); + } + } + + } + + /** + * Traverses a Jar file using the given connection and collects all classes + * which are inside the given 'javaPackage' and can be assigned to the given + * 'baseClass'. The found classes are added to 'result'. + * + * @param javaPackage + * The java package containing the classes (classes may be in a + * sub package) + * @param baseClass + * The class which the target classes extend + * @param result + * The collection to which found classes are added + * @throws IOException + */ + private <T> void findClassesInJar(JarURLConnection juc, String javaPackage, + Class<T> baseClass, Collection<Class<? extends T>> result) + throws IOException { + String javaPackageDir = javaPackage.replace('.', '/'); + Enumeration<JarEntry> ent = juc.getJarFile().entries(); + while (ent.hasMoreElements()) { + JarEntry e = ent.nextElement(); + if (e.getName().endsWith(".class") + && e.getName().startsWith(javaPackageDir)) { + String fullyQualifiedClassName = e.getName().replace('/', '.') + .replace(".class", ""); + addClassIfMatches(result, fullyQualifiedClassName, baseClass); + } + } + } + + /** + * Verifies that the class represented by 'fullyQualifiedClassName' can be + * loaded, assigned to 'baseClass' and is not an abstract or anonymous + * class. + * + * @param result + * The collection to add to + * @param fullyQualifiedClassName + * The candidate class + * @param baseClass + * The class 'fullyQualifiedClassName' should be assignable to + */ + @SuppressWarnings("unchecked") + protected <T> void addClassIfMatches(Collection<Class<? extends T>> result, + String fullyQualifiedClassName, Class<T> baseClass) { + try { + // Try to load the class + + Class<?> c = Class.forName(fullyQualifiedClassName); + if (!baseClass.isAssignableFrom(c)) { + return; + } + if (!includeInSuite(c)) { + return; + } + + if (!Modifier.isAbstract(c.getModifiers()) && !c.isAnonymousClass()) { + result.add((Class<? extends T>) c); + } + } catch (Exception e) { + // Could ignore that class cannot be loaded + e.printStackTrace(); + } catch (LinkageError e) { + // Ignore. Client side classes will at least throw LinkageErrors + } + + } + + /** + * @return true if the class should be included in the suite, false if not + */ + private boolean includeInSuite(Class<?> c) { + if (c.getAnnotation(ExcludeFromSuite.class) != null) { + return false; + } + + IncludeIfProperty includeIfProperty = c + .getAnnotation(IncludeIfProperty.class); + if (includeIfProperty != null) { + String includeValue = includeIfProperty.value(); + String systemPropertyValue = System.getProperty(includeIfProperty + .property()); + if (!includeValue.equals(systemPropertyValue)) { + return false; + } + } + + return true; + } +}
\ No newline at end of file diff --git a/uitest-common/src/main/java/com/vaadin/tests/tb3/TB3TestSuite.java b/uitest-common/src/main/java/com/vaadin/tests/tb3/TB3TestSuite.java new file mode 100644 index 0000000000..c68d59a4eb --- /dev/null +++ b/uitest-common/src/main/java/com/vaadin/tests/tb3/TB3TestSuite.java @@ -0,0 +1,48 @@ +/* + * 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.io.IOException; + +import org.junit.runners.model.InitializationError; + +import com.vaadin.testbench.parallel.ParallelTestSuite; + +/** + * Test suite which consists of all the TB3 tests passed in the constructor. + * Runs the tests in parallel using a {@link ParallelScheduler} + * + * @author Vaadin Ltd + */ +public class TB3TestSuite extends ParallelTestSuite { + + public TB3TestSuite(Class<?> klass, + Class<? extends AbstractTB3Test> baseClass, String basePackage, + String[] ignorePackages) throws InitializationError, IOException { + this(klass, baseClass, basePackage, ignorePackages, + new TB3TestLocator()); + } + + public TB3TestSuite(Class<?> klass, + Class<? extends AbstractTB3Test> baseClass, String basePackage, + String[] ignorePackages, TB3TestLocator locator) + throws InitializationError, IOException { + super(klass, locator + .findClasses(baseClass, basePackage, ignorePackages).toArray( + new Class<?>[] {})); + } +} diff --git a/uitest-common/src/main/java/com/vaadin/tests/tb3/VaadinBrowserFactory.java b/uitest-common/src/main/java/com/vaadin/tests/tb3/VaadinBrowserFactory.java new file mode 100644 index 0000000000..2389811ff0 --- /dev/null +++ b/uitest-common/src/main/java/com/vaadin/tests/tb3/VaadinBrowserFactory.java @@ -0,0 +1,95 @@ +/* + * 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.util.logging.Logger; + +import org.openqa.selenium.Platform; +import org.openqa.selenium.ie.InternetExplorerDriver; +import org.openqa.selenium.remote.DesiredCapabilities; + +import com.vaadin.testbench.parallel.Browser; +import com.vaadin.testbench.parallel.DefaultBrowserFactory; +import com.vaadin.testbench.parallel.TestBenchBrowserFactory; + +public class VaadinBrowserFactory extends DefaultBrowserFactory { + + TestBenchBrowserFactory delegate = null; + + @Override + public DesiredCapabilities create(Browser browser) { + String browserFactoryClass = System.getProperty("browser.factory"); + if (browserFactoryClass != null + && !browserFactoryClass.trim().isEmpty()) { + if (delegate == null) { + getLogger() + .info("Using browser factory " + browserFactoryClass); + try { + delegate = (TestBenchBrowserFactory) getClass() + .getClassLoader().loadClass(browserFactoryClass) + .newInstance(); + } catch (Exception e) { + getLogger().warning( + "Failed to instantiate browser factory " + + browserFactoryClass); + throw new RuntimeException(e); + } + } + return delegate.create(browser); + } + + return doCreate(browser); + } + + public DesiredCapabilities doCreate(Browser browser) { + switch (browser) { + case IE8: + return createIE(browser, "8"); + case IE9: + return createIE(browser, "9"); + case IE10: + return createIE(browser, "10"); + case IE11: + return createIE(browser, "11"); + case PHANTOMJS: + return create(browser, "1", Platform.LINUX); + case CHROME: + return create(browser, "40", Platform.VISTA); + case FIREFOX: + default: + return create(browser, "24", Platform.XP); + } + } + + private DesiredCapabilities createIE(Browser browser, String version) { + DesiredCapabilities capabilities = create(browser, version, + Platform.WINDOWS); + capabilities.setCapability( + InternetExplorerDriver.IE_ENSURE_CLEAN_SESSION, true); + return capabilities; + } + + @Override + public DesiredCapabilities create(Browser browser, String version) { + DesiredCapabilities capabilities = create(browser); + capabilities.setVersion(version); + return capabilities; + } + + private static final Logger getLogger() { + return Logger.getLogger(VaadinBrowserFactory.class.getName()); + } +} |