io.github.kgress.scaffold.webdriver.WebDriverManager Maven / Gradle / Ivy
Show all versions of framework Show documentation
package io.github.kgress.scaffold.webdriver;
import io.github.kgress.scaffold.environment.config.DesiredCapabilitiesConfigurationProperties;
import io.github.kgress.scaffold.environment.config.SeleniumGridServiceConfiguration;
import io.github.kgress.scaffold.exception.WebDriverContextException;
import io.github.kgress.scaffold.models.GridSessionRequest;
import io.github.kgress.scaffold.models.GridSessionResponse;
import io.github.kgress.scaffold.models.enums.BrowserType;
import io.github.kgress.scaffold.models.enums.RunType;
import io.github.kgress.scaffold.models.unittests.MockWebDriver;
import io.github.kgress.scaffold.webdriver.interfaces.TestContextSetting;
import lombok.extern.slf4j.Slf4j;
import org.openqa.selenium.Cookie;
import org.openqa.selenium.MutableCapabilities;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.edge.EdgeDriver;
import org.openqa.selenium.edge.EdgeOptions;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;
import org.openqa.selenium.ie.InternetExplorerDriver;
import org.openqa.selenium.ie.InternetExplorerOptions;
import org.openqa.selenium.opera.OperaDriver;
import org.openqa.selenium.opera.OperaOptions;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.safari.SafariDriver;
import org.openqa.selenium.safari.SafariOptions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.Set;
import java.util.TreeSet;
import static io.github.kgress.scaffold.models.enums.RunType.*;
import static io.github.kgress.scaffold.util.AutomationUtils.getStackTrace;
import static java.util.concurrent.TimeUnit.SECONDS;
/**
* This class manages {@link WebDriverWrapper} instances on a per thread basis. This is useful for multi-threaded tests to be
* able to seamlessly access their associated WebDriver instance statically.
*
* The startLock and closeLock properties:
* We want locks to open *and* close WebDriver instances when we are using Selenium Grid as we do not want a large amount of
* open/close requests flooding the hub at once. This way, they will be processed serially.
*/
@Slf4j
@Service
public class WebDriverManager {
private static final String GRID_TEST_SESSION_URI = "/grid/api/testsession";
private static final Long TEN_SECONDS = 10L;
private final RestTemplate seleniumGridRestTemplate;
private final DesiredCapabilitiesConfigurationProperties desiredCapabilities;
private WebDriverWrapper webDriverWrapper;
private final Object startLock = new Object();
private final Object closeLock = new Object();
private Set cookieJar = new TreeSet<>();
private RunType runType;
private BrowserType browserType;
private String sessionId;
@Autowired
public WebDriverManager(DesiredCapabilitiesConfigurationProperties desiredCapabilities,
RestTemplate seleniumGridRestTemplate) {
this.desiredCapabilities = desiredCapabilities;
this.seleniumGridRestTemplate = seleniumGridRestTemplate;
this.runType = desiredCapabilities.getRunType();
this.browserType = desiredCapabilities.getBrowserType();
}
/**
* Creates a new instance of a {@link WebDriver}, wraps it into a {@link WebDriverWrapper}, and sets an implicit timeout.
*
* 1. Check if the {@link WebDriverWrapper} is null from the {@link WebDriverContext}. If it's null, the driver already exists and
* we've encountered a threading issue.
* 2. Configure the new browser driver.
* 3. Create the new {@link WebDriverWrapper} with the browser driver.
* 4. Setup implicit waits on the driver to make it easier to interact with elements.
*
* @param testName the information on the test that is being ran. This plugs in with Junit Jupiter annotations.
*/
public void initDriver(String testName) {
if (webDriverWrapper != null) {
throw new WebDriverContextException("Driver already exists. Try closing/quitting it before trying to initialize a new one");
}
var browserDriver = configureBrowserDriver(testName);
webDriverWrapper = new WebDriverWrapper(browserDriver);
// Configure the browser to implicitly wait anytime a user attempts to locate an element
webDriverWrapper.manage().timeouts().implicitlyWait(TEN_SECONDS, SECONDS);
}
/**
* Closes the current {@link WebDriver}.
*
* 1. Check if the {@link WebDriverWrapper} is not null. If it's not null, proceed with the closing of the driver.
* 2. Quit the driver on the thread.
*/
public void closeDriver() {
if (webDriverWrapper != null) {
try {
synchronized (closeLock) {
webDriverWrapper.quit();
}
} catch (Exception e) {
log.error("Error quitting browser: " + getStackTrace(e));
}
webDriverWrapper = null;
}
}
/**
* Gets the {@link WebDriverWrapper} on the current thread.
*
* @return the {@link WebDriverWrapper}
*/
public WebDriverWrapper getWebDriverWrapper() {
return webDriverWrapper;
}
public String getSessionId() {
return sessionId;
}
/**
* Set up any cookies the user passed in, so they will be active before the first url is loaded
*
* @param webDriver the instance of the web driver
* @param cookieJar the cookies
*/
private void initializeCookies(WebDriverWrapper webDriver, Set cookieJar) {
if (!cookieJar.isEmpty()) {
for (var cookie : cookieJar) {
webDriver.addCookie(cookie);
}
//If any new cookies were added, refresh the page to make sure they "take".
// Setting them before we get to the correct domain will throw a "same origin" security exception
webDriver.navigate().refresh();
}
}
/**
* Opens the URL.
*
* TODO We should add cookie jar init during the start of the web driver.
*
* @param webDriver the instance of the webdriver to be used
* @param url the url to be navigated to
*/
private void openUrl(WebDriverWrapper webDriver, String url) {
if (url != null && !url.isEmpty()) {
webDriver.get(url);
//Initialize any cookies that should be in place (authentication, traffic slicing, etc)
// before the first navigation is done, then refresh the page
initializeCookies(webDriver, cookieJar);
log.debug(String.format("Opening new WebDriver instance at '%s'", url));
} else {
log.info("No starting URL specified, so no navigation will be performed at this time");
}
}
/**
* Gets the desired capabilities via browser options. Browser options are browser dependent, thus they have their own object.
* However, they all extend off of {@link MutableCapabilities}. Set up the browser options for each browser specific case and
* return those browser options.
*
* This is used during the {@link #configureBrowserDriver(String)} to obtain the capabilities when creating the new Driver.
*
* @return as a browser options object like {@link ChromeOptions}. It must be an object that extends off of {@link MutableCapabilities}
*/
private MutableCapabilities getDesiredCapabilities() {
MutableCapabilities browserOptions;
if (runType != UNIT) {
switch (browserType) {
case Chrome:
browserOptions = new ChromeOptions().setAcceptInsecureCerts(true);
break;
case Safari:
browserOptions = new SafariOptions();
break;
case Firefox:
browserOptions = new FirefoxOptions().setAcceptInsecureCerts(true);
break;
case InternetExplorer:
browserOptions = new InternetExplorerOptions();
break;
case Edge:
browserOptions = new EdgeOptions();
break;
case Opera:
browserOptions = new OperaOptions();
break;
default:
throw new WebDriverContextException("No browser or invalid browser type called for: " + browserType.toString());
}
// If the runTyep is set to GRID, we should set a "uuid" capability for tracking
if (runType == GRID) {
var uuid = TestContext.baseContext().getSetting(String.class, TestContextSetting.TEST_RUN_ID);
browserOptions.setCapability("uuid", uuid);
}
} else {
browserOptions = new MutableCapabilities();
}
return browserOptions;
}
/**
* Configures the WebDriver with the desired capabilities of Chrome, Safari, Firefox, Opera, or Internet Explorer.
*
* @return as {@link WebDriver}
*/
private WebDriver configureBrowserDriver(String testName) {
var browserOptions = getDesiredCapabilities();
WebDriver webDriver;
log.info("Starting driver for test: " + testName);
if (runType == UNIT) {
webDriver = new MockWebDriver();
} else if (runType == LOCAL) {
webDriver = configureLocalBrowser(browserOptions);
} else if (runType == SAUCE || runType == GRID) {
webDriver = configureRemoteBrowser(browserOptions, testName);
} else {
throw new WebDriverContextException("Unknown run type: " + runType);
}
log.info("Driver started for test: " + testName);
return webDriver;
}
/**
* This helper method configures a {@link WebDriver} for local use. We are now using the updated methods for creating
* the new {@link WebDriver}. In this case:
*
* {@link ChromeDriver}
* {@link SafariDriver}
* {@link FirefoxDriver}
* {@link InternetExplorerDriver}
* {@link OperaDriver}
*
* I've noticed that the drivers require properties to be set to indicate where a particular web driver exists on
* the machine. These properties are set as a system property like: webdriver.chrome.driver=path/to/file.
*
*
* @param browserOptions the browser configuration to be used with the new {@link RemoteWebDriver}
* @return the new {@link WebDriver}
*/
private WebDriver configureLocalBrowser(MutableCapabilities browserOptions) {
WebDriver localWebDriver;
log.debug("Tests will be executed locally.");
switch (browserType) {
case Chrome:
log.debug("Chrome chosen as browser type.");
localWebDriver = new ChromeDriver((ChromeOptions) browserOptions);
break;
case Safari:
log.debug("Safari chosen as browser type.");
localWebDriver = new SafariDriver((SafariOptions) browserOptions);
break;
case Firefox:
log.debug("Firefox chosen as browser type.");
localWebDriver = new FirefoxDriver((FirefoxOptions) browserOptions);
break;
case InternetExplorer:
log.debug("Internet Explorer chosen as browser type.");
localWebDriver = new InternetExplorerDriver((InternetExplorerOptions) browserOptions);
break;
case Edge:
log.debug("Edge chosen as browser type.");
localWebDriver = new EdgeDriver((EdgeOptions) browserOptions);
break;
case Opera:
log.debug("Opera chosen as browser type.");
localWebDriver = new OperaDriver((OperaOptions) browserOptions);
break;
default:
throw new WebDriverContextException("No browser or invalid browser type called for: " + browserType.toString());
}
return localWebDriver;
}
/**
* This helper method configures a {@link RemoteWebDriver}. A {@link RemoteWebDriver} is used for testing against
* Selenium Grid or SauceLabs.
*
* @param browserOptions the browser configuration to be used with the new {@link RemoteWebDriver}
* @return the new {@link RemoteWebDriver} as a {@link WebDriver}
*/
private WebDriver configureRemoteBrowser(MutableCapabilities browserOptions, String testName) {
var browserVersion = desiredCapabilities.getBrowserVersion();
var runPlatform = desiredCapabilities.getRunPlatform();
RemoteWebDriver remoteWebDriver;
// If the test is using GRID but the browserOptions are null, throw an error. We must have DesiredCapabilitiesConfigurationProperties configured.
if (browserOptions == null) {
throw new WebDriverContextException("DesiredCapabilitiesConfigurationProperties object was null. This must be initialized to use Grid");
}
// If the browser version isn't null, set the version capability to what the user wants
if (browserVersion != null) {
browserOptions.setCapability("version", browserVersion);
}
// If the run platform value isn't null, set the platform to what the user wants
if (runPlatform != null) {
browserOptions.setCapability("platform", runPlatform);
}
remoteWebDriver = createRemoteWebDriver(browserOptions, testName);
sessionId = remoteWebDriver.getSessionId().toString();
TestContext.baseContext().addSetting("SESSION_ID", sessionId);
checkIfGridAndSendGridRequest(remoteWebDriver);
return remoteWebDriver;
}
/**
* Helper method for {@link #configureRemoteBrowser(MutableCapabilities, String)}.
*
* Checks if the run type from the desiredCapabilities bean is GRID. If it is, it'll pull the session id and send
* a new grid request using the {@link RestTemplate} set up from {@link SeleniumGridServiceConfiguration}.
*
* @param remoteWebDriver the {@link RemoteWebDriver} that was setup from the configuration method.
*/
private void checkIfGridAndSendGridRequest(RemoteWebDriver remoteWebDriver) {
var sessionId = remoteWebDriver.getSessionId().toString();
if (runType == GRID) {
try {
// Pull the session id and add it to the Grid request
var gridSessionRequest = new GridSessionRequest();
gridSessionRequest.setSession(sessionId);
var request = new HttpEntity<>(gridSessionRequest);
var fullPath = GRID_TEST_SESSION_URI + "?session=" + gridSessionRequest.getSession();
seleniumGridRestTemplate.getForObject(fullPath, GridSessionResponse.class, request);
} catch (Exception ex) {
log.error("Unable to call the Selenium Grid", ex);
}
}
}
/**
* Helper method for {@link #configureRemoteBrowser(MutableCapabilities, String)}.
*
* Checks the run type from the desiredCapabilities bean and creates a new {@link RemoteWebDriver}.
*
* @param browserOptions the desired capabilites for the browser
* @param testName the information on the test that is being ran. This plugs in with Junit Jupiter annotations.
* @return the new {@link RemoteWebDriver}
*/
private RemoteWebDriver createRemoteWebDriver(MutableCapabilities browserOptions, String testName) {
RemoteWebDriver remoteWebDriver;
log.debug("Tests will be executed against a Remote Host");
if (runType == GRID) {
remoteWebDriver = configureGridRemoteBrowser(browserOptions);
} else if (runType == SAUCE) {
remoteWebDriver = configureSauceRemoteBrowser(browserOptions, testName);
} else {
throw new WebDriverContextException("Error initializing the Remote Web Driver.");
}
return remoteWebDriver;
}
/**
* Helper method for {@link #createRemoteWebDriver(MutableCapabilities, String)}.
*
* If the {@link RunType} is GRID, this sets up the remote driver session for Selenium Grid.
*
* If any issue is discovered during the starting of this browser, we will throw a {@link WebDriverException} with a
* custom message.
*
* @param browserOptions the desired capabilities we're adding on to
* @return the driver as {@link RemoteWebDriver}
*/
private RemoteWebDriver configureGridRemoteBrowser(MutableCapabilities browserOptions) {
var remoteUrl = desiredCapabilities.getRemoteUrl();
try {
return startScreenshotRemoteDriver(remoteUrl, browserOptions);
} catch (Exception e) {
throw new WebDriverContextException("Error initializing remote session against " + runType.getRunType() +
". Check to ensure your remote url is configured correctly prior to running your tests", e);
}
}
/**
* Helper method for {@link #createRemoteWebDriver(MutableCapabilities, String)}
*
* Sets up the desired capabilities for Sauce and returns the fully configured remote url.
*
* If the {@link RunType} is SAUCE, set the user credentials and add the sauce connect tunnel id to capabilities. Also
* the remote URL is a combination of the username, access key, and sauce url. Additional information on this
* can be found here: https://wiki.saucelabs.com/display/DOCS/Getting+Started+with+Selenium+for+Automated+Website+Testing
*
* If any issue is discovered during the starting of this browser, we will throw a {@link WebDriverException} with a
* custom message.
*
* TODO Let's switch this to W3 standard: https://saucelabs.com/products/open-source-frameworks/selenium/w3c-webdriver-protocol
*
* TODO We also need to update report test pass/fail to sauce so it shows up as pass/fail on the sauce UI
*
* @param browserOptions the desired capabilities we're adding on to
* @param testName the information on the test that is being ran. This plugs in with Junit Jupiter annotations.
* @return the driver as {@link RemoteWebDriver}
*/
private RemoteWebDriver configureSauceRemoteBrowser(MutableCapabilities browserOptions, String testName) {
String remoteUrl;
var username = desiredCapabilities.getSauce().getUserName();
var accessKey = desiredCapabilities.getSauce().getAccessKey();
var sauceUrl = desiredCapabilities.getSauce().getUrl();
var tunnelIdentifier = desiredCapabilities.getSauce().getTunnelIdentifier();
browserOptions.setCapability("tunnelIdentifier", tunnelIdentifier);
browserOptions.setCapability("name", testName);
remoteUrl = URI.create("http://" + username + ":" + accessKey + sauceUrl).toString();
try {
return startScreenshotRemoteDriver(remoteUrl, browserOptions);
} catch (Exception e) {
throw new WebDriverContextException("Error initializing remote session against " + runType.getRunType() +
". Check to ensure your Sauce TunnelIdentifier has been initialized prior to running your tests.", e);
}
}
/**
* Starts a new {@link ScreenshotRemoteDriver} for the remote session.
*
* This method will throw a {@link MalformedURLException}. Only two methods should be using this helper method:
* {@link #configureSauceRemoteBrowser(MutableCapabilities, String)} and
* {@link #configureGridRemoteBrowser(MutableCapabilities)}. Those methods should be responsible for throwing their
* own custom error message since they both have varying reasons that could cause a failure during the initialization
* of a new remote driver.
*
* @param remoteUrl the remote URL to be used
* @param browserOptions the mutable capabilities of the browser
* @return the driver as a {@link RemoteWebDriver}
*/
private synchronized RemoteWebDriver startScreenshotRemoteDriver(String remoteUrl, MutableCapabilities browserOptions)
throws MalformedURLException {
synchronized (startLock) {
return new ScreenshotRemoteDriver(new URL(remoteUrl), browserOptions);
}
}
}