diff options
author | Henri Sara <henri.sara@gmail.com> | 2017-02-16 08:56:32 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-02-16 08:56:32 +0200 |
commit | 7f1cfd03d5b55f41ea911fcd53ce6fd168517cff (patch) | |
tree | 09cc623eef4889bd2194959b6c77dc75ab21cce2 /uitest | |
parent | 888cae67dbaae27d2b505304901d2e387ca6bfaa (diff) | |
download | vaadin-framework-7f1cfd03d5b55f41ea911fcd53ce6fd168517cff.tar.gz vaadin-framework-7f1cfd03d5b55f41ea911fcd53ce6fd168517cff.zip |
Eliminate module uitest-common (#8583)
The use of the module in the test project was removed earlier, and
eliminating the module permits simpler and more reliable builds as
it was not deployed.
The classes that were in uitest-common are now in uitest.
Diffstat (limited to 'uitest')
24 files changed, 3525 insertions, 7 deletions
diff --git a/uitest/pom.xml b/uitest/pom.xml index d2abf4cd7c..5473a9d2f8 100644 --- a/uitest/pom.xml +++ b/uitest/pom.xml @@ -185,13 +185,6 @@ </dependency> <dependency> - <groupId>${project.groupId}</groupId> - <artifactId>vaadin-uitest-common</artifactId> - <version>${project.version}</version> - <scope>test</scope> - </dependency> - - <dependency> <groupId>com.jcraft</groupId> <artifactId>jsch</artifactId> </dependency> @@ -228,6 +221,14 @@ </exclusion> </exclusions> </dependency> + + <dependency> + <groupId>com.vaadin</groupId> + <artifactId>vaadin-testbench-api</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + </dependencies> <build> diff --git a/uitest/src/test/java/com/vaadin/testbench/customelements/AbstractDateFieldElement.java b/uitest/src/test/java/com/vaadin/testbench/customelements/AbstractDateFieldElement.java new file mode 100644 index 0000000000..4d61a6a491 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/testbench/customelements/AbstractDateFieldElement.java @@ -0,0 +1,7 @@ +package com.vaadin.testbench.customelements; + +import com.vaadin.testbench.elementsbase.ServerClass; + +@ServerClass("com.vaadin.ui.AbstractDateField") +public class AbstractDateFieldElement extends DateFieldElement { +} diff --git a/uitest/src/test/java/com/vaadin/testbench/customelements/DateFieldElement.java b/uitest/src/test/java/com/vaadin/testbench/customelements/DateFieldElement.java new file mode 100644 index 0000000000..3c0348ee02 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/testbench/customelements/DateFieldElement.java @@ -0,0 +1,13 @@ +package com.vaadin.testbench.customelements; + +import org.openqa.selenium.By; + +import com.vaadin.testbench.elementsbase.ServerClass; + +@ServerClass("com.vaadin.ui.DateField") +public class DateFieldElement + extends com.vaadin.testbench.elements.DateFieldElement { + public void openPopup() { + findElement(By.tagName("button")).click(); + } +} diff --git a/uitest/src/test/java/com/vaadin/testbench/customelements/DateTimeFieldElement.java b/uitest/src/test/java/com/vaadin/testbench/customelements/DateTimeFieldElement.java new file mode 100644 index 0000000000..022acbfa8a --- /dev/null +++ b/uitest/src/test/java/com/vaadin/testbench/customelements/DateTimeFieldElement.java @@ -0,0 +1,27 @@ +/* + * Copyright 2000-2016 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.testbench.customelements; + +import com.vaadin.testbench.elementsbase.ServerClass; + +/** + * @author Vaadin Ltd + * + */ +@ServerClass("com.vaadin.ui.DateTimeField") +public class DateTimeFieldElement extends DateFieldElement { + +} diff --git a/uitest/src/test/java/com/vaadin/testbench/customelements/InlineDateTimeFieldElement.java b/uitest/src/test/java/com/vaadin/testbench/customelements/InlineDateTimeFieldElement.java new file mode 100644 index 0000000000..27d6d90640 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/testbench/customelements/InlineDateTimeFieldElement.java @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2016 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.testbench.customelements; + +import com.vaadin.testbench.elements.InlineDateFieldElement; +import com.vaadin.testbench.elementsbase.ServerClass; + +/** + * @author Vaadin Ltd + * + */ +@ServerClass("com.vaadin.ui.InlineDateTimeField") +public class InlineDateTimeFieldElement extends InlineDateFieldElement { + +} diff --git a/uitest/src/test/java/com/vaadin/testbench/customelements/LoginFormElement.java b/uitest/src/test/java/com/vaadin/testbench/customelements/LoginFormElement.java new file mode 100644 index 0000000000..59cd0444e7 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/testbench/customelements/LoginFormElement.java @@ -0,0 +1,9 @@ +package com.vaadin.testbench.customelements; + +import com.vaadin.testbench.elements.AbstractSingleComponentContainerElement; +import com.vaadin.testbench.elementsbase.ServerClass; + +@ServerClass("com.vaadin.ui.LoginForm") +public class LoginFormElement extends AbstractSingleComponentContainerElement { + +} diff --git a/uitest/src/test/java/com/vaadin/tests/tb3/AbstractTB3Test.java b/uitest/src/test/java/com/vaadin/tests/tb3/AbstractTB3Test.java new file mode 100644 index 0000000000..0322d76949 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/tb3/AbstractTB3Test.java @@ -0,0 +1,1267 @@ +/* + * Copyright 2000-2016 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.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; + + 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<>(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 + */ + protected void sleep(int timeoutMillis) { + while (timeoutMillis > 0) { + int d = Math.min(BROWSER_TIMEOUT_IN_MS, timeoutMillis); + try { + Thread.sleep(d); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + 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) { + driver.findElement(By.id(id)).click(); + } + + 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<>(); + + 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 if (BrowserUtil.isFirefox(getDesiredCapabilities())) { + // Firefox workaround + getCommandExecutor().executeScript("arguments[0].click()", cb); + } else { + cb.click(); + } + } + + protected void clickElement(WebElement element) { + if (BrowserUtil.isFirefox(getDesiredCapabilities())) { + // Workaround for Selenium/TB and Firefox 45 issue + ((TestBenchElement) (element)).clickHiddenElement(); + } else { + element.click(); + } + } + + protected void contextClickElement(WebElement element) { + if (BrowserUtil.isFirefox(getDesiredCapabilities())) { + // Workaround for Selenium/TB and Firefox 45 issue + getCommandExecutor().executeScript( + "var ev = document.createEvent('HTMLEvents'); ev.initEvent('contextmenu', true, false); arguments[0].dispatchEvent(ev);", + element); + } else { + new Actions(getDriver()).contextClick(element).perform(); + } + } + + protected boolean isLoadingIndicatorVisible() { + WebElement loadingIndicator = findElement( + By.className("v-loading-indicator")); + + return loadingIndicator.isDisplayed(); + } + + protected void waitUntilLoadingIndicatorVisible() { + waitUntil(input -> isLoadingIndicatorVisible()); + } + + protected void waitUntilLoadingIndicatorNotVisible() { + waitUntil(input -> !isLoadingIndicatorVisible()); + } + + /** + * 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 = "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 = "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/src/test/java/com/vaadin/tests/tb3/BrowserStackBrowserFactory.java b/uitest/src/test/java/com/vaadin/tests/tb3/BrowserStackBrowserFactory.java new file mode 100644 index 0000000000..7ab0c33c8a --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/tb3/BrowserStackBrowserFactory.java @@ -0,0 +1,111 @@ +/* + * Copyright 2000-2016 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 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/src/test/java/com/vaadin/tests/tb3/CustomTestBenchCommandExecutor.java b/uitest/src/test/java/com/vaadin/tests/tb3/CustomTestBenchCommandExecutor.java new file mode 100644 index 0000000000..ad5988d571 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/tb3/CustomTestBenchCommandExecutor.java @@ -0,0 +1,156 @@ +/* + * Copyright 2000-2016 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) + 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()); + } + + /** + * 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) throws IOException { + for (int times = 0; times < Parameters + .getMaxScreenshotRetries(); times++) { + BufferedImage screenshotImage = cropToElement(element, + ImageIO.read(new ByteArrayInputStream( + ((TakesScreenshot) actualDriver) + .getScreenshotAs(OutputType.BYTES)))); + 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 + * @return + * @throws IOException + */ + public static BufferedImage cropToElement(WebElement element, + BufferedImage fullScreen) 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 (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/src/test/java/com/vaadin/tests/tb3/ExcludeFromSuite.java b/uitest/src/test/java/com/vaadin/tests/tb3/ExcludeFromSuite.java new file mode 100644 index 0000000000..8dcd813ad8 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/tb3/ExcludeFromSuite.java @@ -0,0 +1,37 @@ +/* + * Copyright 2000-2016 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/src/test/java/com/vaadin/tests/tb3/IncludeIfProperty.java b/uitest/src/test/java/com/vaadin/tests/tb3/IncludeIfProperty.java new file mode 100644 index 0000000000..db3989edb5 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/tb3/IncludeIfProperty.java @@ -0,0 +1,44 @@ +/* + * Copyright 2000-2016 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/src/test/java/com/vaadin/tests/tb3/MultiBrowserTest.java b/uitest/src/test/java/com/vaadin/tests/tb3/MultiBrowserTest.java new file mode 100644 index 0000000000..1193ba9334 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/tb3/MultiBrowserTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2000-2016 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 PhantomJS 1 + return getBrowserCapabilities(Browser.IE11, Browser.FIREFOX, + Browser.CHROME); + } + + protected List<DesiredCapabilities> getBrowsersExcludingPhantomJS() { + return getBrowserCapabilities(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.IE11, Browser.CHROME, + Browser.PHANTOMJS); + } + + protected List<DesiredCapabilities> getBrowsersSupportingShiftClick() { + return getBrowserCapabilities(Browser.IE11, Browser.CHROME); + } + + protected List<DesiredCapabilities> getIEBrowsersOnly() { + return getBrowserCapabilities(Browser.IE11); + } + + protected List<DesiredCapabilities> getBrowsersSupportingContextMenu() { + // context menu doesn't work in phantom JS + return getBrowserCapabilities(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() { + return getBrowserCapabilities(Browser.IE11, Browser.FIREFOX, + Browser.CHROME, Browser.PHANTOMJS); + } + + protected List<DesiredCapabilities> getBrowserCapabilities( + Browser... browsers) { + List<DesiredCapabilities> capabilities = new ArrayList<>(); + for (Browser browser : browsers) { + capabilities.add(browser.getDesiredCapabilities()); + } + return capabilities; + } +} diff --git a/uitest/src/test/java/com/vaadin/tests/tb3/ParallelScheduler.java b/uitest/src/test/java/com/vaadin/tests/tb3/ParallelScheduler.java new file mode 100644 index 0000000000..e270e26c53 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/tb3/ParallelScheduler.java @@ -0,0 +1,69 @@ +/* + * Copyright 2000-2016 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<>(); + 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/src/test/java/com/vaadin/tests/tb3/ParameterizedTB3Runner.java b/uitest/src/test/java/com/vaadin/tests/tb3/ParameterizedTB3Runner.java new file mode 100644 index 0000000000..20107f8058 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/tb3/ParameterizedTB3Runner.java @@ -0,0 +1,181 @@ +/* + * Copyright 2000-2016 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 java.util.Objects; + +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<>(); + + // 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<>(); + 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 + "]"; + }; + + @Override + public boolean equals(Object obj) { + if (!TBMethodWithBefore.class.isInstance(obj)) { + return false; + } + + TBMethodWithBefore otherTbMethod = (TBMethodWithBefore) obj; + + return super.equals(obj) + && Objects.equals(otherTbMethod.parent, parent) + && Objects.equals(otherTbMethod.setter, setter) + && Objects.equals(otherTbMethod.value, value); + } + + } +} diff --git a/uitest/src/test/java/com/vaadin/tests/tb3/PrivateTB3Configuration.java b/uitest/src/test/java/com/vaadin/tests/tb3/PrivateTB3Configuration.java new file mode 100644 index 0000000000..abee2c1c2c --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/tb3/PrivateTB3Configuration.java @@ -0,0 +1,279 @@ +/* + * 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/src/test/java/com/vaadin/tests/tb3/RetryOnFail.java b/uitest/src/test/java/com/vaadin/tests/tb3/RetryOnFail.java new file mode 100644 index 0000000000..cd735ae76b --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/tb3/RetryOnFail.java @@ -0,0 +1,74 @@ +/* + * Copyright 2000-2016 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.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.trim().isEmpty()) { + try { + return Integer.parseInt(retryCount); + } catch (NumberFormatException e) { + // TODO: See how this was implemented in TestBench + Logger.getLogger(RetryOnFail.class.getName()).warning( + "Could not parse max retry count. Retry count set to 0. Failed value: " + + retryCount); + } + } + + return 0; + } + }; + } +} diff --git a/uitest/src/test/java/com/vaadin/tests/tb3/SauceLabsBrowserFactory.java b/uitest/src/test/java/com/vaadin/tests/tb3/SauceLabsBrowserFactory.java new file mode 100644 index 0000000000..e95195d061 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/tb3/SauceLabsBrowserFactory.java @@ -0,0 +1,99 @@ +/* + * Copyright 2000-2016 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 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/src/test/java/com/vaadin/tests/tb3/ScreenshotTB3Test.java b/uitest/src/test/java/com/vaadin/tests/tb3/ScreenshotTB3Test.java new file mode 100644 index 0000000000..ffc4ec9043 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/tb3/ScreenshotTB3Test.java @@ -0,0 +1,498 @@ +/* + * Copyright 2000-2016 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.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<>(); + + 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<>(); + + 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); + } + 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<>(); + 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/src/test/java/com/vaadin/tests/tb3/SingleBrowserTest.java b/uitest/src/test/java/com/vaadin/tests/tb3/SingleBrowserTest.java new file mode 100644 index 0000000000..2bbc0a6420 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/tb3/SingleBrowserTest.java @@ -0,0 +1,34 @@ +/* + * Copyright 2000-2016 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/src/test/java/com/vaadin/tests/tb3/SingleBrowserTestPhantomJS2.java b/uitest/src/test/java/com/vaadin/tests/tb3/SingleBrowserTestPhantomJS2.java new file mode 100644 index 0000000000..6a2e64d2ee --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/tb3/SingleBrowserTestPhantomJS2.java @@ -0,0 +1,29 @@ +/* + * Copyright 2000-2016 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/src/test/java/com/vaadin/tests/tb3/TB3Runner.java b/uitest/src/test/java/com/vaadin/tests/tb3/TB3Runner.java new file mode 100644 index 0000000000..796d62aa50 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/tb3/TB3Runner.java @@ -0,0 +1,81 @@ +/* + * Copyright 2000-2016 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/src/test/java/com/vaadin/tests/tb3/TB3TestLocator.java b/uitest/src/test/java/com/vaadin/tests/tb3/TB3TestLocator.java new file mode 100644 index 0000000000..8635477491 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/tb3/TB3TestLocator.java @@ -0,0 +1,230 @@ +/* + * Copyright 2000-2016 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<>(); + 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/src/test/java/com/vaadin/tests/tb3/TB3TestSuite.java b/uitest/src/test/java/com/vaadin/tests/tb3/TB3TestSuite.java new file mode 100644 index 0000000000..429f827999 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/tb3/TB3TestSuite.java @@ -0,0 +1,47 @@ +/* + * Copyright 2000-2016 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/src/test/java/com/vaadin/tests/tb3/VaadinBrowserFactory.java b/uitest/src/test/java/com/vaadin/tests/tb3/VaadinBrowserFactory.java new file mode 100644 index 0000000000..05d9614129 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/tb3/VaadinBrowserFactory.java @@ -0,0 +1,91 @@ +/* + * Copyright 2000-2016 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 IE11: + return createIE(browser, "11"); + case PHANTOMJS: + return create(browser, "1", Platform.LINUX); + case CHROME: + return create(browser, "40", Platform.VISTA); + case FIREFOX: + default: + DesiredCapabilities dc = create(Browser.FIREFOX, "45", + Platform.WINDOWS); + dc.setCapability("marionette", "false"); + return dc; + } + } + + 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()); + } +} |