Skip to content

Commit 7847039

Browse files
authored
[INT-282] visual-java: retrieve data from browser in bulk when possible (#235)
1 parent 888952d commit 7847039

File tree

7 files changed

+312
-76
lines changed

7 files changed

+312
-76
lines changed

visual-java/src/main/java/com/saucelabs/visual/VisualApi.java

+136-76
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import static com.saucelabs.visual.utils.EnvironmentVariables.isNotBlank;
44
import static com.saucelabs.visual.utils.EnvironmentVariables.valueOrDefault;
55

6+
import com.saucelabs.visual.exception.InvalidIgnoreSelectorException;
7+
import com.saucelabs.visual.exception.InvalidVisualRegionException;
8+
import com.saucelabs.visual.exception.InvalidWebElementException;
69
import com.saucelabs.visual.exception.VisualApiException;
710
import com.saucelabs.visual.graphql.*;
811
import com.saucelabs.visual.graphql.type.*;
@@ -153,6 +156,7 @@ public VisualApi build() {
153156
}
154157

155158
private final GraphQLClient client;
159+
private final BulkDriverHelper bulkDriverHelper;
156160

157161
private final VisualBuild build;
158162
private final String jobId;
@@ -170,7 +174,7 @@ public VisualApi build() {
170174
/**
171175
* Creates a VisualApi instance for a given Visual Backend {@link DataCenter}
172176
*
173-
* @param driver The {@link org.openqa.selenium.WebDriver} instance where the tests should run at
177+
* @param driver The {@link WebDriver} instance where the tests should run at
174178
* @param username SauceLabs username
175179
* @param accessKey SauceLabs access key
176180
*/
@@ -181,7 +185,7 @@ public VisualApi(RemoteWebDriver driver, String username, String accessKey) {
181185
/**
182186
* Creates a VisualApi instance for a given Visual Backend {@link DataCenter}
183187
*
184-
* @param driver The {@link org.openqa.selenium.WebDriver} instance where the tests should run at
188+
* @param driver The {@link WebDriver} instance where the tests should run at
185189
* @param region Visual Backend Region. For available values, see: {@link DataCenter}
186190
* @param username SauceLabs username
187191
* @param accessKey SauceLabs access key
@@ -193,7 +197,7 @@ public VisualApi(RemoteWebDriver driver, DataCenter region, String username, Str
193197
/**
194198
* Creates a VisualApi instance with a custom backend URL
195199
*
196-
* @param driver The {@link org.openqa.selenium.WebDriver} instance where the tests should run at
200+
* @param driver The {@link WebDriver} instance where the tests should run at
197201
* @param url Visual Backend URL
198202
* @param username SauceLabs username
199203
* @param accessKey SauceLabs access key
@@ -205,8 +209,7 @@ public VisualApi(RemoteWebDriver driver, String url, String username, String acc
205209
/**
206210
* Creates a VisualApi instance with a custom backend URL
207211
*
208-
* @param driver The {@link org.openqa.selenium.WebDriver} instance where the tests should run
209-
* with
212+
* @param driver The {@link WebDriver} instance where the tests should run with
210213
* @param url Visual Backend URL
211214
* @param username SauceLabs username
212215
* @param accessKey SauceLabs access key
@@ -224,8 +227,7 @@ public VisualApi(
224227
/**
225228
* Creates a VisualApi instance with a custom backend URL
226229
*
227-
* @param driver The {@link org.openqa.selenium.WebDriver} instance where the tests should run
228-
* with
230+
* @param driver The {@link WebDriver} instance where the tests should run with
229231
* @param url Visual Backend URL
230232
* @param username SauceLabs username
231233
* @param accessKey SauceLabs access key
@@ -265,6 +267,7 @@ private VisualApi(
265267
this.build = VisualBuild.getBuildOnce(this, buildAttributes);
266268
this.driver = driver;
267269
this.isSauceSession = isSauceSession;
270+
this.bulkDriverHelper = new BulkDriverHelper(driver);
268271
refreshWebDriverSessionInfo();
269272
}
270273

@@ -313,6 +316,7 @@ private VisualApi(Builder builder) {
313316
this.driver = driver;
314317
this.client = new GraphQLClient(url, username, accessKey, requestConfig);
315318
this.sessionMetadataBlob = sessionMetadataBlob;
319+
this.bulkDriverHelper = new BulkDriverHelper(driver);
316320
}
317321

318322
/**
@@ -652,24 +656,14 @@ private String sauceVisualCheckLocal(String snapshotName, CheckOptions options)
652656
SnapshotUpload uploadResult =
653657
this.client.execute(mutation, CreateSnapshotUploadMutation.Data.class).result;
654658

655-
// upload image
656-
this.client.upload(uploadResult.getImageUploadUrl(), screenshot, "image/png");
657-
658659
// add ignore regions
659660
List<RegionIn> ignoreRegions = extractIgnoreList(options);
661+
ignoreRegions.addAll(
662+
extractElementsToIgnoreRegions(
663+
Optional.ofNullable(options.getIgnoreElements()).orElse(Collections.emptyList())));
664+
ignoreRegions.addAll(extractIgnoreSelectors(options));
660665

661-
for (WebElement element : options.getIgnoreElements()) {
662-
RegionIn ignoreRegion = VisualRegion.ignoreChangesFor(element).toRegionIn();
663-
ignoreRegions.add(ignoreRegion);
664-
}
665-
666-
for (IgnoreSelectorIn selector : options.getIgnoreSelectors()) {
667-
VisualRegion region = getIgnoreRegionFromSelector(selector);
668-
if (region != null) {
669-
ignoreRegions.add(region.toRegionIn());
670-
}
671-
}
672-
666+
// make regions relative to viewport
673667
List<RegionIn> visibleIgnoreRegions = new ArrayList<>();
674668
for (RegionIn region : ignoreRegions) {
675669
Rectangle regionRect =
@@ -686,6 +680,9 @@ private String sauceVisualCheckLocal(String snapshotName, CheckOptions options)
686680
}
687681
}
688682

683+
// upload image
684+
this.client.upload(uploadResult.getImageUploadUrl(), screenshot, "image/png");
685+
689686
// upload dom if present / enabled
690687
Boolean shouldCaptureDom = Optional.ofNullable(options.getCaptureDom()).orElse(this.captureDom);
691688
if (shouldCaptureDom != null && shouldCaptureDom) {
@@ -790,22 +787,6 @@ private WebElement getClipElement(CheckOptions checkOptions) {
790787
return null;
791788
}
792789

793-
private VisualRegion getIgnoreRegionFromSelector(IgnoreSelectorIn ignoreSelector) {
794-
SelectorIn selector = ignoreSelector.getSelector();
795-
By bySelector;
796-
797-
switch (selector.getType()) {
798-
case XPATH:
799-
bySelector = By.xpath(selector.getValue());
800-
break;
801-
default:
802-
return null;
803-
}
804-
805-
WebElement element = driver.findElement(bySelector);
806-
return new VisualRegion(element, ignoreSelector.getDiffingOptions());
807-
}
808-
809790
private static DiffingMethod toDiffingMethod(CheckOptions options) {
810791
if (options == null || options.getDiffingMethod() == null) {
811792
return DiffingMethod.BALANCED;
@@ -887,33 +868,58 @@ private List<RegionIn> extractIgnoreList(CheckOptions options) {
887868
}
888869

889870
List<IgnoreRegion> ignoredRegions =
890-
options.getIgnoreRegions() == null ? Arrays.asList() : options.getIgnoreRegions();
871+
options.getIgnoreRegions() == null ? Collections.emptyList() : options.getIgnoreRegions();
891872

892873
List<VisualRegion> visualRegions =
893-
options.getRegions() == null ? Arrays.asList() : options.getRegions();
874+
options.getRegions() == null ? Collections.emptyList() : options.getRegions();
894875

895-
List<RegionIn> result = new ArrayList<>();
896-
for (int i = 0; i < ignoredRegions.size(); i++) {
897-
IgnoreRegion ignoreRegion = ignoredRegions.get(i);
898-
if (validate(ignoreRegion) == null) {
899-
throw new VisualApiException("options.ignoreRegion[" + i + "] is an invalid ignore region");
900-
}
901-
result.add(
902-
VisualRegion.ignoreChangesFor(
903-
ignoreRegion.getName(),
904-
ignoreRegion.getX(),
905-
ignoreRegion.getY(),
906-
ignoreRegion.getWidth(),
907-
ignoreRegion.getHeight())
908-
.toRegionIn());
909-
}
910-
for (int i = 0; i < visualRegions.size(); i++) {
911-
VisualRegion region = visualRegions.get(i);
876+
List<VisualRegion> allVisualRegions =
877+
new ArrayList<>(ignoredRegions.size() + visualRegions.size());
878+
allVisualRegions.addAll(visualRegions);
879+
880+
for (IgnoreRegion ignoreRegion : ignoredRegions) {
881+
allVisualRegions.add(new VisualRegion(ignoreRegion));
882+
}
883+
884+
return extractRegions(allVisualRegions);
885+
}
886+
887+
private List<RegionIn> extractRegions(List<VisualRegion> regions) {
888+
List<RegionIn> result = new ArrayList<>(regions.size());
889+
List<WebElement> bulkWebElements = new ArrayList<>();
890+
List<VisualRegion> bulkRegions = new ArrayList<>();
891+
892+
for (VisualRegion region : regions) {
912893
if (validate(region) == null) {
913-
throw new VisualApiException("options.region[" + i + "] is an invalid visual region");
894+
throw new InvalidVisualRegionException(region, "Visual region is invalid");
895+
}
896+
897+
WebElement element = region.getElement();
898+
if (element != null) {
899+
bulkWebElements.add(element);
900+
bulkRegions.add(region);
901+
} else {
902+
result.add(region.toRegionIn());
914903
}
915-
result.add(region.toRegionIn());
916904
}
905+
906+
List<Boolean> bulkIsDisplayed = bulkDriverHelper.areDisplayed(bulkWebElements);
907+
for (int i = 0; i < bulkIsDisplayed.size(); i++) {
908+
VisualRegion region = bulkRegions.get(i);
909+
Boolean isDisplayed = bulkIsDisplayed.get(i);
910+
if (!isDisplayed) {
911+
throw new InvalidVisualRegionException(
912+
region, "Visual region's web element does not exist (yet)");
913+
}
914+
}
915+
916+
List<Rectangle> bulkRectangles = bulkDriverHelper.getRects(bulkWebElements);
917+
for (int i = 0; i < bulkRectangles.size(); i++) {
918+
VisualRegion region = bulkRegions.get(i);
919+
Rectangle rectangle = bulkRectangles.get(i);
920+
result.add(VisualRegion.ignoreChangesFor(region.getName(), rectangle).toRegionIn());
921+
}
922+
917923
return result;
918924
}
919925

@@ -924,31 +930,89 @@ private List<ElementIn> extractIgnoreElements(CheckOptions options) {
924930
: Arrays.asList();
925931

926932
List<ElementIn> result = new ArrayList<>();
927-
for (int i = 0; i < ignoredElements.size(); i++) {
933+
934+
List<Boolean> bulkIsDisplayed = bulkDriverHelper.areDisplayed(ignoredElements);
935+
for (int i = 0; i < bulkIsDisplayed.size(); i++) {
928936
WebElement element = ignoredElements.get(i);
929-
if (validate(element) == null) {
930-
throw new VisualApiException("options.ignoreElement[" + i + "] does not exist (yet)");
937+
Boolean isDisplayed = bulkIsDisplayed.get(i);
938+
if (!isDisplayed) {
939+
throw new InvalidWebElementException(element, "Web element does not exist (yet)");
931940
}
941+
932942
result.add(VisualRegion.ignoreChangesFor(element).toElementIn());
933943
}
944+
934945
return result;
935946
}
936947

937-
private WebElement validate(WebElement element) {
938-
if (element instanceof RemoteWebElement && element.isDisplayed()) {
939-
return element;
948+
private List<RegionIn> extractElementsToIgnoreRegions(List<WebElement> elements) {
949+
List<RegionIn> result = new ArrayList<>();
950+
951+
List<Boolean> bulkIsDisplayed = bulkDriverHelper.areDisplayed(elements);
952+
for (int i = 0; i < bulkIsDisplayed.size(); i++) {
953+
WebElement element = elements.get(i);
954+
Boolean isDisplayed = bulkIsDisplayed.get(i);
955+
if (!isDisplayed) {
956+
throw new InvalidWebElementException(element, "Web element does not exist (yet)");
957+
}
940958
}
941-
return null;
959+
960+
List<Rectangle> bulkRectangles = bulkDriverHelper.getRects(elements);
961+
for (Rectangle rectangle : bulkRectangles) {
962+
result.add(VisualRegion.ignoreChangesFor(rectangle).toRegionIn());
963+
}
964+
965+
return result;
942966
}
943967

944-
private IgnoreRegion validate(IgnoreRegion region) {
945-
if (region == null) {
946-
return null;
968+
private List<RegionIn> extractIgnoreSelectors(CheckOptions options) {
969+
List<IgnoreSelectorIn> selectors =
970+
options != null && options.getIgnoreSelectors() != null
971+
? options.getIgnoreSelectors()
972+
: Arrays.asList();
973+
974+
List<List<WebElement>> elementLists =
975+
bulkDriverHelper.resolveElements(
976+
selectors.stream().map(IgnoreSelectorIn::getSelector).collect(Collectors.toList()));
977+
978+
for (int i = 0; i < selectors.size(); i++) {
979+
List<WebElement> elements = elementLists.get(i);
980+
IgnoreSelectorIn selector = selectors.get(i);
981+
if (elements == null || elements.isEmpty()) {
982+
throw new InvalidIgnoreSelectorException(selector, "Web element does not exist");
983+
}
947984
}
948-
if (region.getHeight() <= 0 || region.getWidth() <= 0) {
949-
return null;
985+
986+
List<RegionIn> result = new ArrayList<>();
987+
988+
List<RegionIn> flatRegions =
989+
extractElementsToIgnoreRegions(
990+
elementLists.stream().flatMap(List::stream).collect(Collectors.toList()));
991+
992+
for (int listIndex = 0, regionIndex = 0; listIndex < elementLists.size(); listIndex++) {
993+
IgnoreSelectorIn selector = selectors.get(listIndex);
994+
List<WebElement> elements = elementLists.get(listIndex);
995+
List<RegionIn> regions = flatRegions.subList(regionIndex, regionIndex + elements.size());
996+
997+
result.addAll(
998+
regions.stream()
999+
.map(
1000+
region ->
1001+
new VisualRegion(
1002+
new IgnoreRegion(
1003+
region.getName(),
1004+
region.getX(),
1005+
region.getY(),
1006+
region.getWidth(),
1007+
region.getHeight()),
1008+
selector.getDiffingOptions())
1009+
.toRegionIn())
1010+
.collect(Collectors.toList()));
1011+
1012+
regionIndex += regions.size();
9501013
}
951-
return region;
1014+
1015+
return result;
9521016
}
9531017

9541018
private VisualRegion validate(VisualRegion region) {
@@ -958,10 +1022,6 @@ private VisualRegion validate(VisualRegion region) {
9581022
if (0 < region.getHeight() * region.getWidth()) {
9591023
return region;
9601024
}
961-
WebElement ele = region.getElement();
962-
if (ele != null && ele.isDisplayed() && ele.getRect() != null) {
963-
return region;
964-
}
9651025
return null;
9661026
}
9671027

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.saucelabs.visual.exception;
2+
3+
import com.saucelabs.visual.graphql.type.IgnoreSelectorIn;
4+
5+
public class InvalidIgnoreSelectorException extends VisualApiException {
6+
private final IgnoreSelectorIn ignoreSelectorIn;
7+
8+
public InvalidIgnoreSelectorException(IgnoreSelectorIn selector, String message) {
9+
super(message);
10+
this.ignoreSelectorIn = selector;
11+
}
12+
13+
public IgnoreSelectorIn getIgnoreSelectorIn() {
14+
return ignoreSelectorIn;
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.saucelabs.visual.exception;
2+
3+
import com.saucelabs.visual.graphql.type.SelectorIn;
4+
5+
public class InvalidSelectorException extends VisualApiException {
6+
private final SelectorIn selectorIn;
7+
8+
public InvalidSelectorException(SelectorIn selector, String message) {
9+
super(message);
10+
this.selectorIn = selector;
11+
}
12+
13+
public SelectorIn getSelectorIn() {
14+
return selectorIn;
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.saucelabs.visual.exception;
2+
3+
import com.saucelabs.visual.model.VisualRegion;
4+
5+
public class InvalidVisualRegionException extends VisualApiException {
6+
private final VisualRegion visualRegion;
7+
8+
public InvalidVisualRegionException(VisualRegion visualRegion, String message) {
9+
super(message);
10+
this.visualRegion = visualRegion;
11+
}
12+
13+
public VisualRegion getVisualRegion() {
14+
return visualRegion;
15+
}
16+
}

0 commit comments

Comments
 (0)