/* * Copyright 2016 GoDataDriven B.V. * * 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 io.divolte.server; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.primitives.Ints; import com.saucelabs.saucerest.SauceREST; import io.divolte.server.ServerTestUtils.TestServer; import org.junit.AssumptionViolatedException; import org.junit.Rule; import org.junit.rules.*; import org.junit.runner.Description; import org.junit.runner.RunWith; import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; import org.junit.runners.model.Statement; import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriverException; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.logging.Logs; import org.openqa.selenium.phantomjs.PhantomJSDriver; import org.openqa.selenium.remote.DesiredCapabilities; import org.openqa.selenium.remote.RemoteWebDriver; import org.openqa.selenium.safari.SafariDriver; import org.openqa.selenium.support.ui.ExpectedCondition; import org.openqa.selenium.support.ui.WebDriverWait; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import javax.annotation.ParametersAreNonnullByDefault; import java.net.MalformedURLException; import java.net.URL; import java.time.Instant; import java.util.*; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Collectors; import static io.divolte.server.BrowserLists.*; @RunWith(ConcurrentParameterized.class) @ParametersAreNonnullByDefault public abstract class SeleniumTestBase { private static final Logger logger = LoggerFactory.getLogger(SeleniumTestBase.class); public static final String DRIVER_ENV_VAR = "SELENIUM_DRIVER"; public static final String PHANTOMJS_DRIVER = "phantomjs"; public static final String CHROME_DRIVER = "chrome"; public static final String SAFARI_DRIVER = "safari"; public static final String SAUCE_DRIVER = "sauce"; public static final String BS_DRIVER = "browserstack"; public static final String SAUCE_USER_NAME_ENV_VAR = "SAUCE_USERNAME"; public static final String SAUCE_API_KEY_ENV_VAR = "SAUCE_ACCESS_KEY"; public static final String SAUCE_HOST_ENV_VAR = "SAUCE_HOST"; public static final String SAUCE_PORT_ENV_VAR = "SAUCE_PORT"; public static final String BS_USER_NAME_ENV_VAR = "BS_USER_NAME"; public static final String BS_API_KEY_ENV_VAR = "BS_API_KEY"; public static final String CHROME_DRIVER_LOCATION_ENV_VAR = "CHROME_DRIVER"; public static final DesiredCapabilities LOCAL_RUN_CAPABILITIES; static { LOCAL_RUN_CAPABILITIES = new DesiredCapabilities(); LOCAL_RUN_CAPABILITIES.setBrowserName("Local Selenium instructed browser"); } private static final ExpectedCondition<Boolean> DIVOLTE_LOADED = driver -> null != driver && "object".equals(((JavascriptExecutor) driver).executeScript("return typeof divolte")); private static final ExpectedCondition<Boolean> WINDOW_AVAILABLE = driver -> null != driver && !driver.getWindowHandles().isEmpty(); @Rule public final TestRule suppressWebDriverNavigationExceptions = (base, description) -> new Statement() { @Override public void evaluate() throws Throwable { try { base.evaluate(); } catch (final WebDriverException e) { if (e.getMessage().contains("history navigation does not work")) { throw new AssumptionViolatedException("Selenium driver doesn't support navigation required for this test.", e); } throw e; } } }; @Rule public final TestName testName = new TestName(); @Rule public final TestRule chain = RuleChain .outerRule(new ExternalResource() { @Override protected void after() { if (null != driver) { driver.quit(); driver = null; } if (null != server) { server.shutdown(); server = null; } testResultHook = Optional.empty(); } }) .around(new TestWatcher() { @Override protected void succeeded(final Description description) { testResultHook.ifPresent(callback -> callback.accept(TestResult.Passed)); } @Override protected void failed(final Throwable e, final Description description) { final TestResult failureType = e instanceof AssumptionViolatedException ? TestResult.Skipped : TestResult.Failed; testResultHook.ifPresent(callback -> callback.accept(failureType)); fetchSeleniumLogs(); } private void fetchSeleniumLogs() { if (null != driver) { final Logs logs = driver.manage().logs(); try { final Set<String> availableLogTypes = logs.getAvailableLogTypes(); logger.info("Available log types from web driver: {}", availableLogTypes); availableLogTypes.forEach(logType -> { logger.debug("Querying selenium {} logs...", logType); logs.get(logType).getAll() .forEach(logEntry -> logger.info("{}: [{}] {}", Instant.ofEpochMilli(logEntry.getTimestamp() * 1000), logEntry.getLevel(), logEntry.getMessage())); }); } catch (final WebDriverException ignoredException) { // Not all browsers support this. Ignore the error. logger.warn("Selenium doesn't support fetching browser logs for this browser. Sorry."); } } } }); @Nullable protected WebDriver driver; @Nullable protected TestServer server; @Parameter(0) public Supplier<DesiredCapabilities> capabilities; @Parameter(1) public String capabilityDescription; @Parameter(2) public boolean quirksMode; private enum TestResult { Passed, Failed, Skipped } private Optional<Consumer<TestResult>> testResultHook = Optional.empty(); @Parameters(name = "Selenium JS test: {1} (quirks-mode={2})") public static Iterable<Object[]> sauceLabBrowsersToTest() { final Collection<Object[]> browserList; if (!System.getenv().containsKey(DRIVER_ENV_VAR)) { browserList = Collections.emptyList(); } else if (SAUCE_DRIVER.equals(System.getenv().get(DRIVER_ENV_VAR))) { browserList = SAUCE_BROWSER_LIST; logger.info("Selenium test running on SauceLabs with these browsers:\n{}", browserNameList(SAUCE_BROWSER_LIST)); } else if (BS_DRIVER.equals(System.getenv().get(DRIVER_ENV_VAR))) { browserList = BS_BROWSER_LIST; logger.info("Selenium test running on BrowserStack with these browsers:\n{}", browserNameList(BS_BROWSER_LIST)); } else { // Parameters are not used for non-sauce tests browserList = ImmutableList.of(new Object[] { (Supplier<DesiredCapabilities>) () -> LOCAL_RUN_CAPABILITIES, "Local JS test run" }); } // For each browser, we need to run in and out of quirks mode. return browserList.stream() .flatMap((browser) -> ImmutableList.of(new Object[] { browser[0], browser[1], false }, new Object[] { browser[0], browser[1], true }).stream()) .collect(Collectors.toList()); } public enum TestPages { BASIC("test-basic-page"), BASIC_COPY("test-basic-page-copy"), PAGE_VIEW_SUPPLIED("test-basic-page-provided-pv-id"), CUSTOM_JAVASCRIPT_NAME("test-custom-javascript-name"), CUSTOM_PAGE_VIEW("test-custom-page-view"), EVENT_COMMIT("test-event-commit"); private final String resourceName; TestPages(final String resourceName) { this.resourceName = Objects.requireNonNull(resourceName); } } private String urlOf(final TestPages page) { Preconditions.checkState(null != server); final String modeString = quirksMode ? "quirks" : "strict"; return String.format("http://%s:%d/%s/%s.html", server.host, server.port, modeString, page.resourceName); } protected String gotoPage(final TestPages page) { Preconditions.checkState(null != driver); String url = urlOf(page); driver.navigate().to(url); // All test pages load Divolte. waitDivolteLoaded(); return url; } protected void doSetUp(final String configFileName) throws Exception { doSetUp(Optional.of(configFileName)); } protected void doSetUp() throws Exception { doSetUp(Optional.empty()); } private void waitDivolteLoaded() { Preconditions.checkState(null != driver); final WebDriverWait wait = new WebDriverWait(driver, 30); wait.until(DIVOLTE_LOADED); } private void doSetUp(final Optional<String> configFileName) throws Exception { final String driverName = System.getenv().getOrDefault(DRIVER_ENV_VAR, PHANTOMJS_DRIVER); switch (driverName) { case CHROME_DRIVER: setupLocalChrome(); break; case SAFARI_DRIVER: driver = new SafariDriver(); break; case SAUCE_DRIVER: setupSauceLabs(); break; case BS_DRIVER: setupBrowserStack(); break; case PHANTOMJS_DRIVER: default: driver = new PhantomJSDriver(); break; } final WebDriverWait wait = new WebDriverWait(driver, 30); wait.until(WINDOW_AVAILABLE); server = configFileName.map(TestServer::new).orElseGet(TestServer::new); } private void setupBrowserStack() throws MalformedURLException { final String bsUserName = Optional .ofNullable(System.getenv(BS_USER_NAME_ENV_VAR)) .orElseThrow(() -> new RuntimeException("When using 'browserstack' as Selenium driver, please set the BrowserStack username " + "in the " + BS_USER_NAME_ENV_VAR + " env var.")); final String bsApiKey = Optional .ofNullable(System.getenv(BS_API_KEY_ENV_VAR)) .orElseThrow(() -> new RuntimeException("When using 'browserstack' as Selenium driver, please set the BrowserStack username " + "in the " + BS_API_KEY_ENV_VAR + " env var.")); final DesiredCapabilities caps = capabilities.get(); // Note: getMethodName() is misleading. It's really the formatted name from the @Parameters annotation. caps.setCapability("job-name", String.format("%s: %s", getClass().getSimpleName(), testName.getMethodName())); driver = new RemoteWebDriver(new URL(String.format("http://%s:%s@hub.browserstack.com/wd/hub", bsUserName, bsApiKey)), caps); } private void setupSauceLabs() throws MalformedURLException { final String sauceUserName = Optional .ofNullable(System.getenv(SAUCE_USER_NAME_ENV_VAR)) .orElseThrow(() -> new RuntimeException("When using 'sauce' as Selenium driver, please set the SauceLabs username " + "in the " + SAUCE_USER_NAME_ENV_VAR + " env var.")); final String sauceApiKey = Optional .ofNullable(System.getenv(SAUCE_API_KEY_ENV_VAR)) .orElseThrow(() -> new RuntimeException("When using 'sauce' as Selenium driver, please set the SauceLabs username " + "in the " + SAUCE_API_KEY_ENV_VAR + " env var.")); final String sauceHost = Optional .ofNullable(System.getenv(SAUCE_HOST_ENV_VAR)) .orElse("localhost"); final int saucePort = Optional .ofNullable(System.getenv(SAUCE_PORT_ENV_VAR)).map(Ints::tryParse) .orElse(4445); final DesiredCapabilities caps = capabilities.get(); // Note: getMethodName() is misleading. It's really the formatted name from the @Parameters annotation. caps.setCapability("name", String.format("%s: %s", getClass().getSimpleName(), testName.getMethodName())); caps.setCapability("public", "team"); caps.setCapability("videoUploadOnPass", false); final RemoteWebDriver remoteDriver = new RemoteWebDriver(new URL(String.format("http://%s:%s@%s:%d/wd/hub", sauceUserName, sauceApiKey, sauceHost, saucePort)), caps); final String sauceJobId = remoteDriver.getSessionId().toString(); final SauceREST sauce = new SauceREST(sauceUserName, sauceApiKey); driver = remoteDriver; testResultHook = Optional.of(result -> { switch (result) { case Passed: sauce.jobPassed(sauceJobId); break; case Skipped: sauce.deleteJob(sauceJobId); break; case Failed: sauce.jobFailed(sauceJobId); } }); } private void setupLocalChrome() { System.setProperty("webdriver.chrome.driver", Optional.ofNullable(System.getenv(CHROME_DRIVER_LOCATION_ENV_VAR)) .orElseThrow( () -> new RuntimeException("When using 'chrome' as Selenium driver, please set the location of the " + "Chrome driver manager server thingie in the env var: " + CHROME_DRIVER_LOCATION_ENV_VAR))); driver = new ChromeDriver(); } }