From cae3407c18a73a3ce92ec8429e8471320a8ac35b Mon Sep 17 00:00:00 2001 From: Benjamin Karran Date: Wed, 8 May 2024 11:42:34 +0200 Subject: [PATCH] Java: Implement visual regions and refactor diffing options (#44) Co-authored-by: Benjamin Karran --- .../com/saucelabs/visual/CheckOptions.java | 80 +++++---- .../java/com/saucelabs/visual/VisualApi.java | 121 +++++-------- .../CreateSnapshotFromWebDriverMutation.java | 2 + .../saucelabs/visual/model/DiffingFlag.java | 27 +++ .../saucelabs/visual/model/DiffingOption.java | 16 -- .../saucelabs/visual/model/IgnoreRegion.java | 26 --- .../saucelabs/visual/model/VisualRegion.java | 168 ++++++++++++++++++ 7 files changed, 291 insertions(+), 149 deletions(-) create mode 100644 visual-java/src/main/java/com/saucelabs/visual/model/DiffingFlag.java delete mode 100644 visual-java/src/main/java/com/saucelabs/visual/model/DiffingOption.java create mode 100644 visual-java/src/main/java/com/saucelabs/visual/model/VisualRegion.java diff --git a/visual-java/src/main/java/com/saucelabs/visual/CheckOptions.java b/visual-java/src/main/java/com/saucelabs/visual/CheckOptions.java index 08fabe12..8fac9c0d 100644 --- a/visual-java/src/main/java/com/saucelabs/visual/CheckOptions.java +++ b/visual-java/src/main/java/com/saucelabs/visual/CheckOptions.java @@ -1,8 +1,12 @@ package com.saucelabs.visual; +import com.saucelabs.visual.graphql.type.DiffingOptionsIn; +import com.saucelabs.visual.model.DiffingFlag; import com.saucelabs.visual.model.FullPageScreenshotConfig; import com.saucelabs.visual.model.IgnoreRegion; +import com.saucelabs.visual.model.VisualRegion; import java.util.ArrayList; +import java.util.EnumSet; import java.util.List; import org.openqa.selenium.WebElement; @@ -19,50 +23,50 @@ public CheckOptions() {} public CheckOptions( List ignoreElements, List ignoreRegions, + List regions, String testName, String suiteName, DiffingMethod diffingMethod, + DiffingOptionsIn diffingOptions, Boolean captureDom, String clipSelector, - FullPageScreenshotConfig fullPageScreenshotConfig, - List enableOnly, - List disableOnly) { + FullPageScreenshotConfig fullPageScreenshotConfig) { this.ignoreElements = ignoreElements; this.ignoreRegions = ignoreRegions; + this.regions = regions; this.testName = testName; this.suiteName = suiteName; this.diffingMethod = diffingMethod; this.captureDom = captureDom; this.clipSelector = clipSelector; this.fullPageScreenshotConfig = fullPageScreenshotConfig; - this.enableOnly = enableOnly; - this.disableOnly = disableOnly; + this.diffingOptions = diffingOptions; } private List ignoreElements = new ArrayList<>(); private List ignoreRegions = new ArrayList<>(); + private List regions = new ArrayList<>(); private String testName; private String suiteName; private DiffingMethod diffingMethod; + private DiffingOptionsIn diffingOptions; private Boolean captureDom; private String clipSelector; private FullPageScreenshotConfig fullPageScreenshotConfig; - private List enableOnly; - private List disableOnly; public static class Builder { private List ignoreElements = new ArrayList<>(); private List ignoreRegions = new ArrayList<>(); + private List regions = new ArrayList<>(); private String testName; private String suiteName; private DiffingMethod diffingMethod; + private DiffingOptionsIn diffingOptions; private Boolean captureDom; private String clipSelector; private FullPageScreenshotConfig fullPageScreenshotConfig; - private List enableOnly; - private List disableOnly; public Builder withIgnoreElements(List ignoreElements) { this.ignoreElements = ignoreElements; @@ -104,13 +108,29 @@ public Builder withFullPageConfig(FullPageScreenshotConfig fullPageScreenshotCon return this; } - public Builder enableOnly(List enableOnly) { - this.enableOnly = enableOnly; + public Builder disableOnly(EnumSet flags) { + this.diffingOptions = new DiffingOptionsIn(); + DiffingFlag.setAll(this.diffingOptions, true); + for (DiffingFlag f : flags) f.apply(this.diffingOptions, false); return this; } - public Builder disableOnly(List disableOnly) { - this.disableOnly = disableOnly; + public Builder enableOnly(EnumSet flags) { + this.diffingOptions = new DiffingOptionsIn(); + DiffingFlag.setAll(this.diffingOptions, false); + for (DiffingFlag f : flags) f.apply(this.diffingOptions, true); + return this; + } + + public Builder enableOnly(EnumSet flags, WebElement element) { + + this.regions.add(VisualRegion.ignoreChangesFor(element).except(flags)); + return this; + } + + public Builder disableOnly(EnumSet flags, WebElement element) { + + this.regions.add(VisualRegion.detectChangesFor(element).except(flags)); return this; } @@ -118,14 +138,14 @@ public CheckOptions build() { return new CheckOptions( ignoreElements, ignoreRegions, + regions, testName, suiteName, diffingMethod, + diffingOptions, captureDom, clipSelector, - fullPageScreenshotConfig, - enableOnly, - disableOnly); + fullPageScreenshotConfig); } } @@ -153,6 +173,14 @@ public void setTestName(String testName) { this.testName = testName; } + public List getRegions() { + return regions; + } + + public void setRegions(List regions) { + this.regions = regions; + } + public String getSuiteName() { return suiteName; } @@ -169,6 +197,10 @@ public DiffingMethod getDiffingMethod() { return diffingMethod; } + public DiffingOptionsIn getDiffingOptions() { + return diffingOptions; + } + public void setCaptureDom(Boolean captureDom) { this.captureDom = captureDom; } @@ -196,20 +228,4 @@ public void enableFullPageScreenshots(FullPageScreenshotConfig fullPageScreensho public void enableFullPageScreenshots() { this.fullPageScreenshotConfig = new FullPageScreenshotConfig.Builder().build(); } - - public void enableOnly(List enableOnly) { - this.enableOnly = enableOnly; - } - - public List getEnableOnly() { - return enableOnly; - } - - public void disableOnly(List disableOnly) { - this.disableOnly = disableOnly; - } - - public List getDisableOnly() { - return disableOnly; - } } diff --git a/visual-java/src/main/java/com/saucelabs/visual/VisualApi.java b/visual-java/src/main/java/com/saucelabs/visual/VisualApi.java index 92768c7d..cd625ebb 100644 --- a/visual-java/src/main/java/com/saucelabs/visual/VisualApi.java +++ b/visual-java/src/main/java/com/saucelabs/visual/VisualApi.java @@ -1,6 +1,5 @@ package com.saucelabs.visual; -import static com.saucelabs.visual.model.DiffingOption.*; import static com.saucelabs.visual.utils.EnvironmentVariables.isNotBlank; import static com.saucelabs.visual.utils.EnvironmentVariables.valueOrDefault; @@ -8,6 +7,7 @@ import com.saucelabs.visual.graphql.*; import com.saucelabs.visual.graphql.type.*; import com.saucelabs.visual.model.IgnoreRegion; +import com.saucelabs.visual.model.VisualRegion; import com.saucelabs.visual.utils.ConsoleColors; import com.saucelabs.visual.utils.EnvironmentVariables; import dev.failsafe.Failsafe; @@ -15,7 +15,6 @@ import java.time.Duration; import java.util.*; import java.util.stream.Collectors; -import org.openqa.selenium.Rectangle; import org.openqa.selenium.WebElement; import org.openqa.selenium.remote.RemoteWebDriver; import org.slf4j.Logger; @@ -330,6 +329,7 @@ public void sauceVisualCheck(String snapshotName, CheckOptions options) { new CreateSnapshotFromWebDriverMutation.CreateSnapshotFromWebDriverIn( this.build.getId(), diffingMethod, + Optional.ofNullable(options.getDiffingOptions()), extractIgnoreList(options), this.jobId, snapshotName, @@ -360,12 +360,6 @@ public void sauceVisualCheck(String snapshotName, CheckOptions options) { input.setFullPageConfig(options.getFullPageScreenshotConfig()); - DiffingOptionsIn diffingOptionsIn = - generateDiffingOptions(options.getEnableOnly(), options.getDisableOnly()); - if (diffingOptionsIn != null) { - input.diffingOptions = Optional.of(diffingOptionsIn); - } - CreateSnapshotFromWebDriverMutation mutation = new CreateSnapshotFromWebDriverMutation(input); CreateSnapshotFromWebDriverMutation.Data check = this.client.execute(mutation, CreateSnapshotFromWebDriverMutation.Data.class); @@ -375,47 +369,6 @@ public void sauceVisualCheck(String snapshotName, CheckOptions options) { } } - private DiffingOptionsIn.Builder setDiffingOptionValue( - DiffingOptionsIn.Builder builder, String key, boolean value) { - switch (key) { - case Content: - return builder.withContent(value); - case Dimensions: - return builder.withDimensions(value); - case Position: - return builder.withPosition(value); - case Structure: - return builder.withStructure(value); - case Style: - return builder.withStyle(value); - case Visual: - return builder.withVisual(value); - } - return builder; - } - - private DiffingOptionsIn generateDiffingOptions( - List enableOnly, List disableOnly) { - if (enableOnly != null && disableOnly != null) { - return null; - } - - DiffingOptionsIn.Builder builder = DiffingOptionsIn.builder(); - - if (enableOnly != null) { - for (String option : DiffingOptionValues) { - setDiffingOptionValue(builder, option, enableOnly.contains(option)); - } - } - - if (disableOnly != null) { - for (String option : DiffingOptionValues) { - setDiffingOptionValue(builder, option, !disableOnly.contains(option)); - } - } - return builder.build(); - } - private static DiffingMethod toDiffingMethod(CheckOptions options) { if (options == null || options.getDiffingMethod() == null) { return null; @@ -495,44 +448,48 @@ private List extractIgnoreList(CheckOptions options) { if (options == null) { return Collections.emptyList(); } + + List ignoredElements = + options.getIgnoreElements() == null ? Arrays.asList() : options.getIgnoreElements(); + + List ignoredRegions = + options.getIgnoreRegions() == null ? Arrays.asList() : options.getIgnoreRegions(); + + List visualRegions = + options.getIgnoreRegions() == null ? Arrays.asList() : options.getRegions(); + List result = new ArrayList<>(); - for (int i = 0; i < options.getIgnoreElements().size(); i++) { - WebElement element = options.getIgnoreElements().get(i); + for (int i = 0; i < ignoredElements.size(); i++) { + WebElement element = ignoredElements.get(i); if (validate(element) == null) { throw new VisualApiException("options.ignoreElement[" + i + "] does not exist (yet)"); } - result.add(toIgnoreIn(element)); + result.add(VisualRegion.ignoreChangesFor(element).toRegionIn()); } - for (int i = 0; i < options.getIgnoreRegions().size(); i++) { - IgnoreRegion ignoreRegion = options.getIgnoreRegions().get(i); + for (int i = 0; i < ignoredRegions.size(); i++) { + IgnoreRegion ignoreRegion = ignoredRegions.get(i); if (validate(ignoreRegion) == null) { throw new VisualApiException("options.ignoreRegion[" + i + "] is an invalid ignore region"); } - result.add(toIgnoreIn(ignoreRegion)); + result.add( + VisualRegion.ignoreChangesFor( + ignoreRegion.getName(), + ignoreRegion.getX(), + ignoreRegion.getHeight(), + ignoreRegion.getWidth(), + ignoreRegion.getHeight()) + .toRegionIn()); + } + for (int i = 0; i < visualRegions.size(); i++) { + VisualRegion region = visualRegions.get(i); + if (validate(region) == null) { + throw new VisualApiException("options.region[" + i + "] is an invalid visual region"); + } + result.add(region.toRegionIn()); } return result; } - private RegionIn toIgnoreIn(WebElement element) { - Rectangle r = element.getRect(); - return RegionIn.builder() - .withX(r.getX()) - .withY(r.getY()) - .withWidth(r.getWidth()) - .withHeight(r.getHeight()) - .build(); - } - - private RegionIn toIgnoreIn(IgnoreRegion r) { - return RegionIn.builder() - .withX(r.getX()) - .withY(r.getY()) - .withWidth(r.getWidth()) - .withHeight(r.getHeight()) - .withDiffingOptions(generateDiffingOptions(r.getEnableOnly(), r.getDisableOnly())) - .build(); - } - private WebElement validate(WebElement element) { if (element == null || !element.isDisplayed() || element.getRect() == null) { return null; @@ -550,6 +507,20 @@ private IgnoreRegion validate(IgnoreRegion region) { return region; } + private VisualRegion validate(VisualRegion region) { + if (region == null) { + return null; + } + if (0 < region.getHeight() * region.getWidth()) { + return region; + } + WebElement ele = region.getElement(); + if (ele != null && ele.isDisplayed() && ele.getRect() != null) { + return region; + } + return null; + } + public VisualBuild getBuild() { return build; } diff --git a/visual-java/src/main/java/com/saucelabs/visual/graphql/CreateSnapshotFromWebDriverMutation.java b/visual-java/src/main/java/com/saucelabs/visual/graphql/CreateSnapshotFromWebDriverMutation.java index 60376b5e..f5f33f59 100644 --- a/visual-java/src/main/java/com/saucelabs/visual/graphql/CreateSnapshotFromWebDriverMutation.java +++ b/visual-java/src/main/java/com/saucelabs/visual/graphql/CreateSnapshotFromWebDriverMutation.java @@ -47,6 +47,7 @@ public static class CreateSnapshotFromWebDriverIn { public CreateSnapshotFromWebDriverIn( String buildUuid, DiffingMethod diffingMethod, + Optional diffingOptions, List ignoreRegions, String jobId, String name, @@ -54,6 +55,7 @@ public CreateSnapshotFromWebDriverIn( String sessionMetadata) { this.buildUuid = buildUuid; this.diffingMethod = diffingMethod; + this.diffingOptions = diffingOptions; this.ignoreRegions = ignoreRegions; this.jobId = jobId; this.name = name; diff --git a/visual-java/src/main/java/com/saucelabs/visual/model/DiffingFlag.java b/visual-java/src/main/java/com/saucelabs/visual/model/DiffingFlag.java new file mode 100644 index 00000000..60b587f2 --- /dev/null +++ b/visual-java/src/main/java/com/saucelabs/visual/model/DiffingFlag.java @@ -0,0 +1,27 @@ +package com.saucelabs.visual.model; + +import com.saucelabs.visual.graphql.type.DiffingOptionsIn; + +public enum DiffingFlag { + Content, + Dimensions, + Position, + Structure, + Style, + Visual; + + public void apply(DiffingOptionsIn options, boolean value) { + if (this == Content) options.setContent(value); + if (this == Dimensions) options.setDimensions(value); + if (this == Position) options.setPosition(value); + if (this == Structure) options.setStructure(value); + if (this == Style) options.setStyle(value); + if (this == Visual) options.setVisual(value); + } + + public static void setAll(DiffingOptionsIn options, boolean value) { + for (DiffingFlag o : DiffingFlag.values()) { + o.apply(options, value); + } + } +} diff --git a/visual-java/src/main/java/com/saucelabs/visual/model/DiffingOption.java b/visual-java/src/main/java/com/saucelabs/visual/model/DiffingOption.java deleted file mode 100644 index cf888b8c..00000000 --- a/visual-java/src/main/java/com/saucelabs/visual/model/DiffingOption.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.saucelabs.visual.model; - -import java.util.Arrays; -import java.util.List; - -public class DiffingOption { - public static final String Content = "content"; - public static final String Dimensions = "dimensions"; - public static final String Position = "position"; - public static final String Structure = "structure"; - public static final String Style = "style"; - public static final String Visual = "visual"; - - public static final List DiffingOptionValues = - Arrays.asList(Content, Dimensions, Position, Structure, Style, Visual); -} diff --git a/visual-java/src/main/java/com/saucelabs/visual/model/IgnoreRegion.java b/visual-java/src/main/java/com/saucelabs/visual/model/IgnoreRegion.java index 6b0375cf..b38f87e7 100644 --- a/visual-java/src/main/java/com/saucelabs/visual/model/IgnoreRegion.java +++ b/visual-java/src/main/java/com/saucelabs/visual/model/IgnoreRegion.java @@ -13,8 +13,6 @@ public class IgnoreRegion { private int width; private int x; private int y; - private List enableOnly; - private List disableOnly; public IgnoreRegion(String name, int x, int y, int width, int height) { this.name = name; @@ -72,30 +70,6 @@ public void setY(int y) { this.y = y; } - public void disableOnly(List options) { - disableOnly = options; - } - - public void setDisableOnly(List disableOnly) { - this.disableOnly = disableOnly; - } - - public List getDisableOnly() { - return disableOnly; - } - - public void enableOnly(List options) { - enableOnly = options; - } - - public void setEnableOnly(List enableOnly) { - this.enableOnly = enableOnly; - } - - public List getEnableOnly() { - return enableOnly; - } - public static List forElement(WebDriver driver, List elements) { JavascriptExecutor js = (JavascriptExecutor) driver; diff --git a/visual-java/src/main/java/com/saucelabs/visual/model/VisualRegion.java b/visual-java/src/main/java/com/saucelabs/visual/model/VisualRegion.java new file mode 100644 index 00000000..94fc75fc --- /dev/null +++ b/visual-java/src/main/java/com/saucelabs/visual/model/VisualRegion.java @@ -0,0 +1,168 @@ +package com.saucelabs.visual.model; + +import com.saucelabs.visual.graphql.type.DiffingOptionsIn; +import com.saucelabs.visual.graphql.type.RegionIn; +import java.util.EnumSet; +import org.openqa.selenium.Rectangle; +import org.openqa.selenium.WebElement; + +public class VisualRegion { + private DiffingOptionsIn options; + private boolean isIgnoreRegion; + private WebElement element; + private int x; + private int y; + private int width; + private int height; + private String name; + + private VisualRegion() {} + + public VisualRegion(IgnoreRegion ir) { + this.options = setAllFlags(new DiffingOptionsIn(), false); + this.isIgnoreRegion = true; + this.name = ir.getName(); + this.height = ir.getHeight(); + this.width = ir.getWidth(); + this.x = ir.getX(); + this.y = ir.getY(); + } + + public VisualRegion(WebElement element, DiffingOptionsIn diffingOptions) { + this.options = diffingOptions; + this.element = element; + } + + public static VisualRegion ignoreChangesFor(WebElement element) { + VisualRegion r = new VisualRegion(); + r.options = setAllFlags(new DiffingOptionsIn(), false); + r.isIgnoreRegion = true; + r.element = element; + return r; + } + + public static VisualRegion detectChangesFor(WebElement element) { + VisualRegion r = new VisualRegion(); + r.options = setAllFlags(new DiffingOptionsIn(), true); + r.isIgnoreRegion = false; + r.element = element; + return r; + } + + public static VisualRegion ignoreChangesFor(String name, int x, int y, int width, int height) { + VisualRegion r = new VisualRegion(); + r.options = setAllFlags(new DiffingOptionsIn(), false); + r.isIgnoreRegion = true; + r.name = name; + r.height = height; + r.width = width; + r.x = x; + r.y = y; + return r; + } + + public static VisualRegion ignoreChangesFor(int x, int y, int width, int height) { + return VisualRegion.ignoreChangesFor("", x, y, width, height); + } + + public static VisualRegion detectChangesFor(String name, int x, int y, int width, int height) { + VisualRegion r = new VisualRegion(); + r.options = setAllFlags(new DiffingOptionsIn(), true); + r.isIgnoreRegion = false; + r.name = name; + r.height = height; + r.width = width; + r.x = x; + r.y = y; + return r; + } + + public static VisualRegion detectChangesFor(int x, int y, int width, int height) { + return VisualRegion.detectChangesFor("", x, y, width, height); + } + + private static DiffingOptionsIn setAllFlags(DiffingOptionsIn opt, boolean value) { + opt.setContent(value); + opt.setDimensions(value); + opt.setPosition(value); + opt.setStructure(value); + opt.setStyle(value); + opt.setVisual(value); + return opt; + } + + public WebElement getElement() { + return element; + } + + public void setElement(WebElement value) { + this.element = value; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = width; + } + + public int getX() { + return x; + } + + public void setX(int x) { + this.x = x; + } + + public int getY() { + return y; + } + + public void setY(int y) { + this.y = y; + } + + public VisualRegion except(EnumSet flags) { + for (DiffingFlag f : flags) { + f.apply(this.options, this.isIgnoreRegion); + } + return this; + } + + public RegionIn toRegionIn() { + RegionIn r = new RegionIn(); + r.setName(this.name); + r.setHeight(this.height); + r.setWidth(this.width); + r.setX(this.x); + r.setY(this.y); + r.setDiffingOptions(this.options); + + if (this.element != null) { + Rectangle rect = element.getRect(); + r.setX(rect.getX()); + r.setY(rect.getY()); + r.setWidth(rect.getWidth()); + r.setHeight(rect.getHeight()); + } + + return r; + } +}