diff --git a/pom.xml b/pom.xml index 31951e2..3b96d87 100644 --- a/pom.xml +++ b/pom.xml @@ -81,6 +81,12 @@ httpclient 4.5.13 + + org.mockito + mockito-core + 3.12.4 + test + diff --git a/src/main/java/io/percy/selenium/Percy.java b/src/main/java/io/percy/selenium/Percy.java index 0f1c191..c8e8508 100644 --- a/src/main/java/io/percy/selenium/Percy.java +++ b/src/main/java/io/percy/selenium/Percy.java @@ -1,5 +1,6 @@ package io.percy.selenium; +import org.apache.commons.exec.util.StringUtils; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; @@ -14,19 +15,17 @@ import org.json.JSONObject; import java.io.InputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.HashMap; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; -import org.openqa.selenium.JavascriptExecutor; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.*; +import org.openqa.selenium.remote.*; + +import java.lang.reflect.Field; /** * Percy client for visual testing. @@ -53,6 +52,9 @@ public class Percy { // Environment information like Java, browser, & SDK versions private Environment env; + // Fetch following properties from capabilities + private final List capsNeeded = new ArrayList<>(Arrays.asList("browserName", "platform", "platformName", "version", "osVersion", "proxy")); + private final String ignoreElementKey = "ignore_region_selenium_elements"; /** * @param driver The Selenium WebDriver object that will hold the browser * session to snapshot. @@ -164,6 +166,79 @@ public void snapshot(String name, Map options) { postSnapshot(domSnapshot, name, driver.getCurrentUrl(), options); } + /** + * Take a snapshot and upload it to Percy. + * + * @param name The human-readable name of the screenshot. Should be unique. + */ + public void screenshot(String name) throws UnsupportedOperationException { + Map options = new HashMap(); + screenshot(name, options); + } + + /** + * Take a snapshot and upload it to Percy. + * + * @param name The human-readable name of the screenshot. Should be unique. + * @param options Extra options + */ + public void screenshot(String name, Map options) throws UnsupportedOperationException { + if (!isPercyEnabled) { return; } + List driverArray = Arrays.asList(driver.getClass().toString().split("\\$")); // Added to handle testcase (mocked driver) + Iterator driverIterator = driverArray.iterator(); + String driverClass = driverIterator.next(); + if (!driverClass.equals(RemoteWebDriver.class.toString())) { throw new UnsupportedOperationException( + String.format("Driver should be of type RemoteWebDriver, passed is %s", driverClass) + ); } + + String sessionId = ((RemoteWebDriver) driver).getSessionId().toString(); + CommandExecutor executor = ((RemoteWebDriver) driver).getCommandExecutor(); + + // Get HttpCommandExecutor From TracedCommandExecutor + if (executor instanceof TracedCommandExecutor) { + Class className = executor.getClass(); + try { + Field field = className.getDeclaredField("delegate"); + // make private field accessible + field.setAccessible(true); + executor = (HttpCommandExecutor)field.get(executor); + } catch (Exception e) { + log(e.toString()); + return; + } + } + String remoteWebAddress = ((HttpCommandExecutor) executor).getAddressOfRemoteServer().toString(); + + Capabilities caps = ((RemoteWebDriver) driver).getCapabilities(); + ConcurrentHashMap capabilities = new ConcurrentHashMap(); + + Iterator iterator = capsNeeded.iterator(); + while (iterator.hasNext()) { + String cap = iterator.next(); + if (caps.getCapability(cap) != null) { + capabilities.put(cap, caps.getCapability(cap).toString()); + } + } + + if (options.containsKey(ignoreElementKey)) { + List ignoreElementIds = getElementIdFromElement((List) options.get(ignoreElementKey)); + options.remove(ignoreElementKey); + options.put("ignore_region_elements", ignoreElementIds); + } + + // Build a JSON object to POST back to the agent node process + JSONObject json = new JSONObject(); + json.put("sessionId", sessionId); + json.put("commandExecutorUrl", remoteWebAddress); + json.put("capabilities", capabilities); + json.put("snapshotName", name); + json.put("clientInfo", env.getClientInfo()); + json.put("environmentInfo", env.getEnvironmentInfo()); + json.put("options", options); + + request("/percy/automateScreenshot", json, name); + } + /** * Checks to make sure the local Percy server is running. If not, disable Percy. */ @@ -266,17 +341,27 @@ private void postSnapshot( json.put("clientInfo", env.getClientInfo()); json.put("environmentInfo", env.getEnvironmentInfo()); + request("/percy/snapshot", json, name); + } + + /** + * POST data to the Percy Agent node process. + * + * @param url Endpoint to be called. + * @param name The human-readable name of the snapshot. Should be unique. + * @param json Json object of all properties. + */ + protected void request(String url, JSONObject json, String name) { StringEntity entity = new StringEntity(json.toString(), ContentType.APPLICATION_JSON); try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) { - HttpPost request = new HttpPost(PERCY_SERVER_ADDRESS + "/percy/snapshot"); + HttpPost request = new HttpPost(PERCY_SERVER_ADDRESS + url); request.setEntity(entity); HttpResponse response = httpClient.execute(request); } catch (Exception ex) { if (PERCY_DEBUG) { log(ex.toString()); } log("Could not post snapshot " + name); } - } /** @@ -291,6 +376,15 @@ private String buildSnapshotJS(Map options) { return jsBuilder.toString(); } + private List getElementIdFromElement(List elements) { + List ignoredElementsArray = new ArrayList<>(); + for (int index = 0; index < elements.size(); index++) { + String elementId = elements.get(index).getId(); + ignoredElementsArray.add(elementId); + } + return ignoredElementsArray; + } + private void log(String message) { System.out.println(LABEL + " " + message); } diff --git a/src/test/java/io/percy/selenium/SdkTest.java b/src/test/java/io/percy/selenium/SdkTest.java index 4a8499e..e36d25c 100644 --- a/src/test/java/io/percy/selenium/SdkTest.java +++ b/src/test/java/io/percy/selenium/SdkTest.java @@ -9,6 +9,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + import org.openqa.selenium.By; import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.Keys; @@ -19,7 +21,10 @@ import io.github.bonigarcia.wdm.WebDriverManager; -public class SdkTest { +import org.openqa.selenium.remote.*; +import static org.mockito.Mockito.*; +import java.net.URL; + public class SdkTest { private static final String TEST_URL = "http://localhost:8000"; private static WebDriver driver; private static Percy percy; @@ -108,4 +113,50 @@ public void snapshotWithOptions() { options.put("widths", Arrays.asList(768, 992, 1200)); percy.snapshot("Site with options", options); } + + @Test + public void takeScreenshotWhenNonRemoteWebDriver() { + assertThrows(UnsupportedOperationException.class, () -> { + percy.screenshot("Test"); + }); + } + @Test + public void takeScreenshot() { + RemoteWebDriver mockedDriver = mock(RemoteWebDriver.class); + HttpCommandExecutor commandExecutor = mock(HttpCommandExecutor.class); + try { + when(commandExecutor.getAddressOfRemoteServer()).thenReturn(new URL("https://hub-cloud.browserstack.com/wd/hub")); + } catch (Exception e) { + } + percy = spy(new Percy(mockedDriver)); + when(mockedDriver.getSessionId()).thenReturn(new SessionId("123")); + when(mockedDriver.getCommandExecutor()).thenReturn(commandExecutor); + DesiredCapabilities capabilities = new DesiredCapabilities(); + capabilities.setCapability("browserName", "Chrome"); + when(mockedDriver.getCapabilities()).thenReturn(capabilities); + percy.screenshot("Test"); + verify(percy).request(eq("/percy/automateScreenshot"), any(), eq("Test")); + } + + @Test + public void takeScreenshotWithOptions() { + RemoteWebDriver mockedDriver = mock(RemoteWebDriver.class); + HttpCommandExecutor commandExecutor = mock(HttpCommandExecutor.class); + try { + when(commandExecutor.getAddressOfRemoteServer()).thenReturn(new URL("https://hub-cloud.browserstack.com/wd/hub")); + } catch (Exception e) { + } + percy = spy(new Percy(mockedDriver)); + when(mockedDriver.getSessionId()).thenReturn(new SessionId("123")); + when(mockedDriver.getCommandExecutor()).thenReturn(commandExecutor); + DesiredCapabilities capabilities = new DesiredCapabilities(); + capabilities.setCapability("browserName", "Chrome"); + when(mockedDriver.getCapabilities()).thenReturn(capabilities); + Map options = new HashMap(); + RemoteWebElement mockedElement = mock(RemoteWebElement.class); + when(mockedElement.getId()).thenReturn("1234"); + options.put("ignore_region_selenium_elements", Arrays.asList(mockedElement)); + percy.screenshot("Test", options); + verify(percy).request(eq("/percy/automateScreenshot"), any() , eq("Test")); + } }