Skip to content

Commit a211749

Browse files
chinmay-browserstackAmit3200samarsaultbstack-security-github
authored
✨ Add PoA Support (#216)
* POA changes * Remove extra print * Undo healthcheck changes * added tests and options in param * add mockito dependency * compatable version with java 8 * resolve comment * fix concurrent error in test * fix concurrent access issue * Fix null pointer in test * changed according to comments * add detail for debug failed testcase * update mockito to fix testcases * fix for test cases * fix concurrent access 1 * Addresed comments * POA Support for ignoreRegion (#220) * Added logic for ignore region * added test case * Added validation in tests * Possible concurrent thread fix 1 * Possible concurrent thread fix 2 * Possible concurrent thread fix 3 * Possible concurrent thread fix 4 * Possible concurrent thread fix 5 * Ignore Element key name change (#221) * Added logic for ignore region * added test case * Added validation in tests * Possible concurrent thread fix 1 * Possible concurrent thread fix 2 * Possible concurrent thread fix 3 * Possible concurrent thread fix 4 * Possible concurrent thread fix 5 * Fix ignore region logic * Fixing java sdk for missing property (#226) * CI Add validation for branch input (#215) * Adding Code Scanner Semgrep.yml workflow file --------- Co-authored-by: Amit Singh Sansoya <[email protected]> Co-authored-by: Samarjeet <[email protected]> Co-authored-by: bstack-security-github <[email protected]>
1 parent df1f731 commit a211749

File tree

3 files changed

+162
-11
lines changed

3 files changed

+162
-11
lines changed

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@
8181
<artifactId>httpclient</artifactId>
8282
<version>4.5.13</version>
8383
</dependency>
84+
<dependency>
85+
<groupId>org.mockito</groupId>
86+
<artifactId>mockito-core</artifactId>
87+
<version>3.12.4</version>
88+
<scope>test</scope>
89+
</dependency>
8490

8591
</dependencies>
8692

src/main/java/io/percy/selenium/Percy.java

Lines changed: 104 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.percy.selenium;
22

3+
import org.apache.commons.exec.util.StringUtils;
34
import org.apache.http.Header;
45
import org.apache.http.HttpEntity;
56
import org.apache.http.HttpResponse;
@@ -14,19 +15,17 @@
1415
import org.json.JSONObject;
1516

1617
import java.io.InputStream;
17-
import java.util.ArrayList;
18-
import java.util.Arrays;
19-
import java.util.List;
20-
import java.util.Map;
21-
import java.util.HashMap;
18+
import java.util.*;
19+
import java.util.concurrent.ConcurrentHashMap;
2220
import java.util.logging.Level;
2321
import java.util.logging.Logger;
2422

2523
import javax.annotation.Nullable;
2624

27-
import org.openqa.selenium.JavascriptExecutor;
28-
import org.openqa.selenium.WebDriver;
29-
import org.openqa.selenium.WebDriverException;
25+
import org.openqa.selenium.*;
26+
import org.openqa.selenium.remote.*;
27+
28+
import java.lang.reflect.Field;
3029

3130
/**
3231
* Percy client for visual testing.
@@ -53,6 +52,9 @@ public class Percy {
5352
// Environment information like Java, browser, & SDK versions
5453
private Environment env;
5554

55+
// Fetch following properties from capabilities
56+
private final List<String> capsNeeded = new ArrayList<>(Arrays.asList("browserName", "platform", "platformName", "version", "osVersion", "proxy"));
57+
private final String ignoreElementKey = "ignore_region_selenium_elements";
5658
/**
5759
* @param driver The Selenium WebDriver object that will hold the browser
5860
* session to snapshot.
@@ -164,6 +166,79 @@ public void snapshot(String name, Map<String, Object> options) {
164166
postSnapshot(domSnapshot, name, driver.getCurrentUrl(), options);
165167
}
166168

169+
/**
170+
* Take a snapshot and upload it to Percy.
171+
*
172+
* @param name The human-readable name of the screenshot. Should be unique.
173+
*/
174+
public void screenshot(String name) throws UnsupportedOperationException {
175+
Map<String, Object> options = new HashMap<String, Object>();
176+
screenshot(name, options);
177+
}
178+
179+
/**
180+
* Take a snapshot and upload it to Percy.
181+
*
182+
* @param name The human-readable name of the screenshot. Should be unique.
183+
* @param options Extra options
184+
*/
185+
public void screenshot(String name, Map<String, Object> options) throws UnsupportedOperationException {
186+
if (!isPercyEnabled) { return; }
187+
List<String> driverArray = Arrays.asList(driver.getClass().toString().split("\\$")); // Added to handle testcase (mocked driver)
188+
Iterator<String> driverIterator = driverArray.iterator();
189+
String driverClass = driverIterator.next();
190+
if (!driverClass.equals(RemoteWebDriver.class.toString())) { throw new UnsupportedOperationException(
191+
String.format("Driver should be of type RemoteWebDriver, passed is %s", driverClass)
192+
); }
193+
194+
String sessionId = ((RemoteWebDriver) driver).getSessionId().toString();
195+
CommandExecutor executor = ((RemoteWebDriver) driver).getCommandExecutor();
196+
197+
// Get HttpCommandExecutor From TracedCommandExecutor
198+
if (executor instanceof TracedCommandExecutor) {
199+
Class className = executor.getClass();
200+
try {
201+
Field field = className.getDeclaredField("delegate");
202+
// make private field accessible
203+
field.setAccessible(true);
204+
executor = (HttpCommandExecutor)field.get(executor);
205+
} catch (Exception e) {
206+
log(e.toString());
207+
return;
208+
}
209+
}
210+
String remoteWebAddress = ((HttpCommandExecutor) executor).getAddressOfRemoteServer().toString();
211+
212+
Capabilities caps = ((RemoteWebDriver) driver).getCapabilities();
213+
ConcurrentHashMap<String, String> capabilities = new ConcurrentHashMap<String, String>();
214+
215+
Iterator<String> iterator = capsNeeded.iterator();
216+
while (iterator.hasNext()) {
217+
String cap = iterator.next();
218+
if (caps.getCapability(cap) != null) {
219+
capabilities.put(cap, caps.getCapability(cap).toString());
220+
}
221+
}
222+
223+
if (options.containsKey(ignoreElementKey)) {
224+
List<String> ignoreElementIds = getElementIdFromElement((List<RemoteWebElement>) options.get(ignoreElementKey));
225+
options.remove(ignoreElementKey);
226+
options.put("ignore_region_elements", ignoreElementIds);
227+
}
228+
229+
// Build a JSON object to POST back to the agent node process
230+
JSONObject json = new JSONObject();
231+
json.put("sessionId", sessionId);
232+
json.put("commandExecutorUrl", remoteWebAddress);
233+
json.put("capabilities", capabilities);
234+
json.put("snapshotName", name);
235+
json.put("clientInfo", env.getClientInfo());
236+
json.put("environmentInfo", env.getEnvironmentInfo());
237+
json.put("options", options);
238+
239+
request("/percy/automateScreenshot", json, name);
240+
}
241+
167242
/**
168243
* Checks to make sure the local Percy server is running. If not, disable Percy.
169244
*/
@@ -266,17 +341,27 @@ private void postSnapshot(
266341
json.put("clientInfo", env.getClientInfo());
267342
json.put("environmentInfo", env.getEnvironmentInfo());
268343

344+
request("/percy/snapshot", json, name);
345+
}
346+
347+
/**
348+
* POST data to the Percy Agent node process.
349+
*
350+
* @param url Endpoint to be called.
351+
* @param name The human-readable name of the snapshot. Should be unique.
352+
* @param json Json object of all properties.
353+
*/
354+
protected void request(String url, JSONObject json, String name) {
269355
StringEntity entity = new StringEntity(json.toString(), ContentType.APPLICATION_JSON);
270356

271357
try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
272-
HttpPost request = new HttpPost(PERCY_SERVER_ADDRESS + "/percy/snapshot");
358+
HttpPost request = new HttpPost(PERCY_SERVER_ADDRESS + url);
273359
request.setEntity(entity);
274360
HttpResponse response = httpClient.execute(request);
275361
} catch (Exception ex) {
276362
if (PERCY_DEBUG) { log(ex.toString()); }
277363
log("Could not post snapshot " + name);
278364
}
279-
280365
}
281366

282367
/**
@@ -291,6 +376,15 @@ private String buildSnapshotJS(Map<String, Object> options) {
291376
return jsBuilder.toString();
292377
}
293378

379+
private List<String> getElementIdFromElement(List<RemoteWebElement> elements) {
380+
List<String> ignoredElementsArray = new ArrayList<>();
381+
for (int index = 0; index < elements.size(); index++) {
382+
String elementId = elements.get(index).getId();
383+
ignoredElementsArray.add(elementId);
384+
}
385+
return ignoredElementsArray;
386+
}
387+
294388
private void log(String message) {
295389
System.out.println(LABEL + " " + message);
296390
}

src/test/java/io/percy/selenium/SdkTest.java

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import org.junit.jupiter.api.AfterEach;
1010
import org.junit.jupiter.api.BeforeAll;
1111
import org.junit.jupiter.api.Test;
12+
import static org.junit.jupiter.api.Assertions.*;
13+
1214
import org.openqa.selenium.By;
1315
import org.openqa.selenium.JavascriptExecutor;
1416
import org.openqa.selenium.Keys;
@@ -19,7 +21,10 @@
1921

2022
import io.github.bonigarcia.wdm.WebDriverManager;
2123

22-
public class SdkTest {
24+
import org.openqa.selenium.remote.*;
25+
import static org.mockito.Mockito.*;
26+
import java.net.URL;
27+
public class SdkTest {
2328
private static final String TEST_URL = "http://localhost:8000";
2429
private static WebDriver driver;
2530
private static Percy percy;
@@ -108,4 +113,50 @@ public void snapshotWithOptions() {
108113
options.put("widths", Arrays.asList(768, 992, 1200));
109114
percy.snapshot("Site with options", options);
110115
}
116+
117+
@Test
118+
public void takeScreenshotWhenNonRemoteWebDriver() {
119+
assertThrows(UnsupportedOperationException.class, () -> {
120+
percy.screenshot("Test");
121+
});
122+
}
123+
@Test
124+
public void takeScreenshot() {
125+
RemoteWebDriver mockedDriver = mock(RemoteWebDriver.class);
126+
HttpCommandExecutor commandExecutor = mock(HttpCommandExecutor.class);
127+
try {
128+
when(commandExecutor.getAddressOfRemoteServer()).thenReturn(new URL("https://hub-cloud.browserstack.com/wd/hub"));
129+
} catch (Exception e) {
130+
}
131+
percy = spy(new Percy(mockedDriver));
132+
when(mockedDriver.getSessionId()).thenReturn(new SessionId("123"));
133+
when(mockedDriver.getCommandExecutor()).thenReturn(commandExecutor);
134+
DesiredCapabilities capabilities = new DesiredCapabilities();
135+
capabilities.setCapability("browserName", "Chrome");
136+
when(mockedDriver.getCapabilities()).thenReturn(capabilities);
137+
percy.screenshot("Test");
138+
verify(percy).request(eq("/percy/automateScreenshot"), any(), eq("Test"));
139+
}
140+
141+
@Test
142+
public void takeScreenshotWithOptions() {
143+
RemoteWebDriver mockedDriver = mock(RemoteWebDriver.class);
144+
HttpCommandExecutor commandExecutor = mock(HttpCommandExecutor.class);
145+
try {
146+
when(commandExecutor.getAddressOfRemoteServer()).thenReturn(new URL("https://hub-cloud.browserstack.com/wd/hub"));
147+
} catch (Exception e) {
148+
}
149+
percy = spy(new Percy(mockedDriver));
150+
when(mockedDriver.getSessionId()).thenReturn(new SessionId("123"));
151+
when(mockedDriver.getCommandExecutor()).thenReturn(commandExecutor);
152+
DesiredCapabilities capabilities = new DesiredCapabilities();
153+
capabilities.setCapability("browserName", "Chrome");
154+
when(mockedDriver.getCapabilities()).thenReturn(capabilities);
155+
Map<String, Object> options = new HashMap<String, Object>();
156+
RemoteWebElement mockedElement = mock(RemoteWebElement.class);
157+
when(mockedElement.getId()).thenReturn("1234");
158+
options.put("ignore_region_selenium_elements", Arrays.asList(mockedElement));
159+
percy.screenshot("Test", options);
160+
verify(percy).request(eq("/percy/automateScreenshot"), any() , eq("Test"));
161+
}
111162
}

0 commit comments

Comments
 (0)