From a48e0573efc7f05dd040bf2735fd3ef4f760a8c7 Mon Sep 17 00:00:00 2001 From: Uladzislau Arlouski Date: Tue, 21 Jun 2022 13:49:35 +0300 Subject: [PATCH] [visual] Move ignores cut logic into ashot strategy (#2724) --- .github/workflows/gradle.yml | 4 +- .../databind/ui}/LocatorDeserializer.java | 2 +- .../AbstractAdjustingCoordsProvider.java | 56 ------ .../screenshot/AbstractAshotFactory.java | 16 ++ .../selenium}/screenshot/IgnoreStrategy.java | 4 +- .../screenshot/ScreenshotCropper.java | 98 ++++++++++ .../AbstractScreenshotParametersFactory.java | 70 +++++-- .../screenshot/ScreenshotConfiguration.java | 25 +++ .../ui/screenshot/ScreenshotParameters.java | 16 ++ .../ScreenshotParametersFactory.java | 7 + .../shooting/ElementCroppingDecorator.java | 72 ++++++++ .../resources/vividus-extension/spring.xml | 10 + .../ui}/LocatorDeserializerTests.java | 2 +- .../AbstractAdjustingCoordsProviderTests.java | 90 --------- .../screenshot/AbstractAshotFactoryTests.java | 23 ++- .../screenshot/ScreenshotCropperTests.java | 138 ++++++++++++++ ...tractScreenshotParametersFactoryTests.java | 38 +++- .../ScreenshotConfigurationTests.java | 51 ++++++ .../screenshot/ScreenshotParametersTests.java | 16 ++ .../ElementCroppingDecoratorTests.java | 85 +++++++++ .../selenium/screenshot/after_cropping.png | Bin 0 -> 25914 bytes .../selenium/screenshot/element_cropped.png | Bin 0 -> 28908 bytes .../vividus/selenium/screenshot/original.png | Bin 0 -> 34735 bytes .../visual/AbstractVisualCheckFactory.java | 64 ------- .../visual/model/AbstractVisualCheck.java | 15 -- .../screenshot/AshotScreenshotProvider.java | 99 ---------- ...shotProvider.java => BaselineIndexer.java} | 23 ++- .../visual/steps/AbstractVisualSteps.java | 39 +++- .../resources/vividus-extension/spring.xml | 16 +- .../visual/VisualCheckFactoryTests.java | 93 ---------- .../visual/model/VisualCheckTests.java | 2 - .../AshotScreenshotProviderTests.java | 173 ------------------ .../screenshot/BaselineIndexerTests.java | 58 ++++++ .../screenshot/IgnoreStrategyTests.java | 3 +- .../visual/steps/VisualStepsTests.java | 103 +++++++++-- vividus-plugin-applitools/build.gradle | 1 - ...ableToApplitoolsVisualChecksConverter.java | 3 +- .../visual/eyes/VisualTestingSteps.java | 26 ++- .../factory/ApplitoolsVisualCheckFactory.java | 23 ++- .../visual/eyes/factory/ImageEyesFactory.java | 5 +- .../eyes/model/ApplitoolsVisualCheck.java | 17 +- .../service/ImageVisualTestingService.java | 20 +- .../src/main/resources/spring.xml | 17 +- ...oApplitoolsVisualChecksConverterTests.java | 3 +- .../visual/eyes/VisualTestingStepsTests.java | 21 +++ .../ApplitoolsVisualCheckFactoryTests.java | 13 +- .../model/ApplitoolsVisualCheckTests.java | 22 +-- .../ImageVisualTestingServiceTests.java | 11 +- .../screenshot/MobileAppAshotFactory.java | 8 +- .../screenshot/MobileAppCoordsProvider.java | 44 ++++- .../MobileAppScreenshotParametersFactory.java | 30 ++- .../src/main/resources/spring.xml | 7 +- .../MobileAppAshotFactoryTests.java | 23 ++- .../MobileAppCoordsProviderTests.java | 87 ++++++--- ...leAppScreenshotParametersFactoryTests.java | 26 +++ .../vividus/visual/VisualCheckFactory.java | 56 ------ .../java/org/vividus/visual/VisualSteps.java | 162 +++++++++------- .../visual/engine/VisualTestingEngine.java | 14 +- .../src/main/resources/spring.xml | 5 +- .../visual/VisualCheckFactoryTests.java | 113 ------------ .../org/vividus/visual/VisualStepsTests.java | 120 ++++++------ .../engine/VisualTestingEngineTests.java | 32 +--- .../visual/model/VisualCheckTests.java | 2 - ...crollBarHidingCoordsProviderDecorator.java | 43 +++++ .../selenium/screenshot/WebAshotFactory.java | 36 +++- ...dsProvider.java => WebCoordsProvider.java} | 17 +- .../screenshot/WebScreenshotCropper.java | 62 +++++++ .../WebScreenshotParametersFactory.java | 59 ++++-- .../shooting/ScrollbarHidingDecorator.java} | 37 ++-- .../src/main/resources/spring.xml | 10 +- ...BarHidingCoordsProviderDecoratorTests.java | 59 ++++++ .../screenshot/WebAshotFactoryTests.java | 56 ++++-- ...Tests.java => WebCoordsProviderTests.java} | 25 +-- .../screenshot/WebScreenshotCropperTests.java | 92 ++++++++++ .../WebScreenshotParametersFactoryTests.java | 24 +++ .../ScrollbarHidingDecoratorTests.java} | 48 +++-- .../baselines/ipad-ignore-element.png | Bin 0 -> 19237 bytes .../saucelabs/ipad/environment.properties | 1 + .../suite/system/ipad/suite.properties | 1 + .../story/system/IPadVisualStepsTests.story | 9 + 80 files changed, 1788 insertions(+), 1213 deletions(-) rename {vividus-plugin-web-app/src/main/java/org/vividus/jackson/databind/ui/web => vividus-extension-selenium/src/main/java/org/vividus/jackson/databind/ui}/LocatorDeserializer.java (96%) delete mode 100644 vividus-extension-selenium/src/main/java/org/vividus/selenium/screenshot/AbstractAdjustingCoordsProvider.java rename {vividus-extension-visual-testing/src/main/java/org/vividus/visual => vividus-extension-selenium/src/main/java/org/vividus/selenium}/screenshot/IgnoreStrategy.java (96%) create mode 100644 vividus-extension-selenium/src/main/java/org/vividus/selenium/screenshot/ScreenshotCropper.java create mode 100644 vividus-extension-selenium/src/main/java/ru/yandex/qatools/ashot/shooting/ElementCroppingDecorator.java rename {vividus-plugin-web-app/src/test/java/org/vividus/jackson/databind/ui/web => vividus-extension-selenium/src/test/java/org/vividus/jackson/databind/ui}/LocatorDeserializerTests.java (97%) delete mode 100644 vividus-extension-selenium/src/test/java/org/vividus/selenium/screenshot/AbstractAdjustingCoordsProviderTests.java create mode 100644 vividus-extension-selenium/src/test/java/org/vividus/selenium/screenshot/ScreenshotCropperTests.java create mode 100644 vividus-extension-selenium/src/test/java/org/vividus/ui/screenshot/ScreenshotConfigurationTests.java create mode 100644 vividus-extension-selenium/src/test/java/ru/yandex/qatools/ashot/shooting/ElementCroppingDecoratorTests.java create mode 100644 vividus-extension-selenium/src/test/resources/org/vividus/selenium/screenshot/after_cropping.png create mode 100644 vividus-extension-selenium/src/test/resources/org/vividus/selenium/screenshot/element_cropped.png create mode 100644 vividus-extension-selenium/src/test/resources/org/vividus/selenium/screenshot/original.png delete mode 100644 vividus-extension-visual-testing/src/main/java/org/vividus/visual/AbstractVisualCheckFactory.java delete mode 100644 vividus-extension-visual-testing/src/main/java/org/vividus/visual/screenshot/AshotScreenshotProvider.java rename vividus-extension-visual-testing/src/main/java/org/vividus/visual/screenshot/{ScreenshotProvider.java => BaselineIndexer.java} (51%) delete mode 100644 vividus-extension-visual-testing/src/test/java/org/vividus/visual/VisualCheckFactoryTests.java delete mode 100644 vividus-extension-visual-testing/src/test/java/org/vividus/visual/screenshot/AshotScreenshotProviderTests.java create mode 100644 vividus-extension-visual-testing/src/test/java/org/vividus/visual/screenshot/BaselineIndexerTests.java delete mode 100644 vividus-plugin-visual/src/main/java/org/vividus/visual/VisualCheckFactory.java delete mode 100644 vividus-plugin-visual/src/test/java/org/vividus/visual/VisualCheckFactoryTests.java create mode 100644 vividus-plugin-web-app/src/main/java/org/vividus/selenium/screenshot/ScrollBarHidingCoordsProviderDecorator.java rename vividus-plugin-web-app/src/main/java/org/vividus/selenium/screenshot/{WebAdjustingCoordsProvider.java => WebCoordsProvider.java} (79%) create mode 100644 vividus-plugin-web-app/src/main/java/org/vividus/selenium/screenshot/WebScreenshotCropper.java rename vividus-plugin-web-app/src/main/java/{org/vividus/selenium/screenshot/ScrollbarHidingAshot.java => ru/yandex/qatools/ashot/shooting/ScrollbarHidingDecorator.java} (51%) create mode 100644 vividus-plugin-web-app/src/test/java/org/vividus/selenium/screenshot/ScrollBarHidingCoordsProviderDecoratorTests.java rename vividus-plugin-web-app/src/test/java/org/vividus/selenium/screenshot/{WebAdjustingCoordsProviderTests.java => WebCoordsProviderTests.java} (81%) create mode 100644 vividus-plugin-web-app/src/test/java/org/vividus/selenium/screenshot/WebScreenshotCropperTests.java rename vividus-plugin-web-app/src/test/java/{org/vividus/selenium/screenshot/ScrollbarHidingAshotTests.java => ru/yandex/qatools/ashot/shooting/ScrollbarHidingDecoratorTests.java} (61%) create mode 100644 vividus-tests/src/main/resources/baselines/ipad-ignore-element.png create mode 100644 vividus-tests/src/main/resources/properties/environment/system/saucelabs/ipad/environment.properties create mode 100644 vividus-tests/src/main/resources/properties/suite/system/ipad/suite.properties create mode 100644 vividus-tests/src/main/resources/story/system/IPadVisualStepsTests.story diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index d599ee5e75..927834039a 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -242,8 +242,8 @@ jobs: if [[ -n $SAUCELABS_USER && -n $SAUCELABS_KEY ]]; then declare -a profiles=( ipad ) for profile in "${profiles[@]}"; do - ./gradlew :vividus-tests:debugStories -Pvividus.configuration.environments=system/saucelabs \ - -Pvividus.configuration.suites=grid \ + ./gradlew :vividus-tests:debugStories -Pvividus.configuration.environments=system/saucelabs/${profile} \ + -Pvividus.configuration.suites=system/${profile} \ -Pvividus.configuration.profiles=saucelabs/web,web/tablet/${profile} \ -Pvividus.selenium.grid.username=${SAUCELABS_USER} \ -Pvividus.selenium.grid.password=${SAUCELABS_KEY} diff --git a/vividus-plugin-web-app/src/main/java/org/vividus/jackson/databind/ui/web/LocatorDeserializer.java b/vividus-extension-selenium/src/main/java/org/vividus/jackson/databind/ui/LocatorDeserializer.java similarity index 96% rename from vividus-plugin-web-app/src/main/java/org/vividus/jackson/databind/ui/web/LocatorDeserializer.java rename to vividus-extension-selenium/src/main/java/org/vividus/jackson/databind/ui/LocatorDeserializer.java index b96bfc168f..7bf1d1e232 100644 --- a/vividus-plugin-web-app/src/main/java/org/vividus/jackson/databind/ui/web/LocatorDeserializer.java +++ b/vividus-extension-selenium/src/main/java/org/vividus/jackson/databind/ui/LocatorDeserializer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.vividus.jackson.databind.ui.web; +package org.vividus.jackson.databind.ui; import java.io.IOException; diff --git a/vividus-extension-selenium/src/main/java/org/vividus/selenium/screenshot/AbstractAdjustingCoordsProvider.java b/vividus-extension-selenium/src/main/java/org/vividus/selenium/screenshot/AbstractAdjustingCoordsProvider.java deleted file mode 100644 index 1256d66097..0000000000 --- a/vividus-extension-selenium/src/main/java/org/vividus/selenium/screenshot/AbstractAdjustingCoordsProvider.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2019-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.vividus.selenium.screenshot; - -import org.openqa.selenium.SearchContext; -import org.openqa.selenium.WebElement; -import org.vividus.ui.context.IUiContext; - -import ru.yandex.qatools.ashot.coordinates.Coords; -import ru.yandex.qatools.ashot.coordinates.WebDriverCoordsProvider; - -public abstract class AbstractAdjustingCoordsProvider extends WebDriverCoordsProvider -{ - private static final long serialVersionUID = -7145089672020971479L; - private final transient IUiContext uiContext; - - protected AbstractAdjustingCoordsProvider(IUiContext uiContext) - { - this.uiContext = uiContext; - } - - protected Coords adjustToSearchContext(Coords coords) - { - SearchContext searchContext = uiContext.getSearchContext(); - if (searchContext instanceof WebElement) - { - Coords searchContextCoords = getCoords((WebElement) searchContext); - Coords intersected = coords.intersection(searchContextCoords); - intersected.x = intersected.x - searchContextCoords.x; - intersected.y = intersected.y - searchContextCoords.y; - return intersected; - } - return coords; - } - - protected abstract Coords getCoords(WebElement element); - - protected IUiContext getUiContext() - { - return uiContext; - } -} diff --git a/vividus-extension-selenium/src/main/java/org/vividus/selenium/screenshot/AbstractAshotFactory.java b/vividus-extension-selenium/src/main/java/org/vividus/selenium/screenshot/AbstractAshotFactory.java index 60a024616c..2af9a38e2f 100644 --- a/vividus-extension-selenium/src/main/java/org/vividus/selenium/screenshot/AbstractAshotFactory.java +++ b/vividus-extension-selenium/src/main/java/org/vividus/selenium/screenshot/AbstractAshotFactory.java @@ -19,6 +19,7 @@ import static ru.yandex.qatools.ashot.shooting.ShootingStrategies.cutting; import java.util.Map; +import java.util.Optional; import java.util.function.BiFunction; import javax.inject.Inject; @@ -27,6 +28,7 @@ import org.vividus.selenium.screenshot.strategies.ScreenshotShootingStrategy; import org.vividus.ui.screenshot.ScreenshotParameters; +import ru.yandex.qatools.ashot.shooting.ElementCroppingDecorator; import ru.yandex.qatools.ashot.shooting.ShootingStrategies; import ru.yandex.qatools.ashot.shooting.ShootingStrategy; import ru.yandex.qatools.ashot.shooting.cutter.CutStrategy; @@ -37,6 +39,13 @@ public abstract class AbstractAshotFactory imple private Map strategies; private String screenshotShootingStrategy; + private final ScreenshotCropper screenshotCropper; + + protected AbstractAshotFactory(ScreenshotCropper screenshotCropper) + { + this.screenshotCropper = screenshotCropper; + } + protected ShootingStrategy decorateWithFixedCutStrategy(ShootingStrategy original, int headerToCut, int footerToCut) { return decorateWithCutStrategy(original, headerToCut, footerToCut, FixedCutStrategy::new); @@ -50,6 +59,13 @@ protected ShootingStrategy decorateWithCutStrategy(ShootingStrategy original, in : original; } + protected ShootingStrategy decorateWithCropping(ShootingStrategy strategy, + Optional screenshotParameters) + { + return new ElementCroppingDecorator(strategy, screenshotCropper, + screenshotParameters.map(ScreenshotParameters::getIgnoreStrategies).orElse(Map.of())); + } + protected ScreenshotShootingStrategy getStrategyBy(String strategyName) { ScreenshotShootingStrategy strategy = strategies.get(strategyName); diff --git a/vividus-extension-visual-testing/src/main/java/org/vividus/visual/screenshot/IgnoreStrategy.java b/vividus-extension-selenium/src/main/java/org/vividus/selenium/screenshot/IgnoreStrategy.java similarity index 96% rename from vividus-extension-visual-testing/src/main/java/org/vividus/visual/screenshot/IgnoreStrategy.java rename to vividus-extension-selenium/src/main/java/org/vividus/selenium/screenshot/IgnoreStrategy.java index 0dbc1fc48e..628f77f6ef 100644 --- a/vividus-extension-visual-testing/src/main/java/org/vividus/visual/screenshot/IgnoreStrategy.java +++ b/vividus-extension-selenium/src/main/java/org/vividus/selenium/screenshot/IgnoreStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2020 the original author or authors. + * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.vividus.visual.screenshot; +package org.vividus.selenium.screenshot; import java.awt.Color; import java.awt.Graphics2D; diff --git a/vividus-extension-selenium/src/main/java/org/vividus/selenium/screenshot/ScreenshotCropper.java b/vividus-extension-selenium/src/main/java/org/vividus/selenium/screenshot/ScreenshotCropper.java new file mode 100644 index 0000000000..606e78cc59 --- /dev/null +++ b/vividus-extension-selenium/src/main/java/org/vividus/selenium/screenshot/ScreenshotCropper.java @@ -0,0 +1,98 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.vividus.selenium.screenshot; + +import java.awt.Point; +import java.awt.image.BufferedImage; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.openqa.selenium.WebDriver; +import org.vividus.selenium.IWebDriverProvider; +import org.vividus.ui.action.ISearchActions; +import org.vividus.ui.action.search.Locator; + +import ru.yandex.qatools.ashot.coordinates.Coords; +import ru.yandex.qatools.ashot.coordinates.CoordsProvider; + +public class ScreenshotCropper +{ + private final ScreenshotDebugger screenshotDebugger; + private final ISearchActions searchActions; + private final CoordsProvider coordsProvider; + private final IWebDriverProvider webDriverProvider; + + public ScreenshotCropper(ScreenshotDebugger screenshotDebugger, ISearchActions searchActions, + CoordsProvider coordsProvider, IWebDriverProvider webDriverProvider) + { + this.screenshotDebugger = screenshotDebugger; + this.searchActions = searchActions; + this.coordsProvider = coordsProvider; + this.webDriverProvider = webDriverProvider; + } + + public BufferedImage crop(BufferedImage image, Optional contextCoords, + Map> ignoreStrategies, int topAdjustment) + { + BufferedImage outputImage = image; + WebDriver webDriver = webDriverProvider.get(); + + for (IgnoreStrategy strategy : List.of(IgnoreStrategy.ELEMENT, IgnoreStrategy.AREA)) + { + Set ignore = Optional.ofNullable(ignoreStrategies.get(strategy)).orElse(Set.of()).stream() + .map(searchActions::findElements) + .flatMap(Collection::stream) + .collect(Collectors.collectingAndThen( + Collectors.toList(), + webElementsToIgnore -> coordsProvider.ofElements(webDriver, webElementsToIgnore) + )); + if (!ignore.isEmpty()) + { + Point adjustment = calculateAdjustment(contextCoords, topAdjustment); + ignore.forEach(c -> + { + c.x += adjustment.x; + c.y += adjustment.y; + }); + + outputImage = strategy.crop(outputImage, ignore); + screenshotDebugger.debug(this.getClass(), "cropped_by_" + strategy, outputImage); + } + } + + return outputImage; + } + + protected Point calculateAdjustment(Optional contextCoords, int topAdjustment) + { + return new Point(0, -topAdjustment); + } + + protected CoordsProvider getCoordsProvider() + { + return coordsProvider; + } + + protected IWebDriverProvider getWebDriverProvider() + { + return webDriverProvider; + } +} diff --git a/vividus-extension-selenium/src/main/java/org/vividus/ui/screenshot/AbstractScreenshotParametersFactory.java b/vividus-extension-selenium/src/main/java/org/vividus/ui/screenshot/AbstractScreenshotParametersFactory.java index f18779ce7e..54fcbb1c63 100644 --- a/vividus-extension-selenium/src/main/java/org/vividus/ui/screenshot/AbstractScreenshotParametersFactory.java +++ b/vividus-extension-selenium/src/main/java/org/vividus/ui/screenshot/AbstractScreenshotParametersFactory.java @@ -16,31 +16,41 @@ package org.vividus.ui.screenshot; +import java.util.Collection; +import java.util.EnumMap; +import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.BinaryOperator; -import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.lang3.Validate; +import org.vividus.selenium.screenshot.IgnoreStrategy; +import org.vividus.ui.action.search.Locator; import org.vividus.util.property.PropertyMappedCollection; -public abstract class AbstractScreenshotParametersFactory - implements ScreenshotParametersFactory +public abstract class AbstractScreenshotParametersFactory implements ScreenshotParametersFactory { private PropertyMappedCollection screenshotConfigurations; private String shootingStrategy; + private Map> ignoreStrategies; + + protected Optional getDefaultConfiguration() + { + return screenshotConfigurations.getNullable(shootingStrategy); + } protected Optional getScreenshotConfiguration(Optional screenshotConfiguration, BinaryOperator merger) { - Optional defaultConfiguration = screenshotConfigurations.getNullable(shootingStrategy); + Optional defaultConfiguration = getDefaultConfiguration(); if (screenshotConfiguration.isEmpty()) { return defaultConfiguration; } - else if (defaultConfiguration.isPresent()) - { - return Optional.of(merger.apply(screenshotConfiguration.get(), defaultConfiguration.get())); - } - return screenshotConfiguration; + return defaultConfiguration.map(c -> merger.apply(screenshotConfiguration.get(), c)) + .or(() -> screenshotConfiguration); } protected int ensureValidCutSize(int value, String key) @@ -49,15 +59,46 @@ protected int ensureValidCutSize(int value, String key) return value; } - protected R createWithBaseConfiguration(ScreenshotConfiguration configuration, - Supplier parametersFactory) + protected P createWithBaseConfiguration(ScreenshotConfiguration configuration) { - R parameters = parametersFactory.get(); + Map> stepIgnores = Map.of( + IgnoreStrategy.ELEMENT, configuration.getElementsToIgnore(), + IgnoreStrategy.AREA, configuration.getAreasToIgnore() + ); + + return createWithBaseConfiguration(configuration, stepIgnores); + } + + protected P createWithBaseConfiguration(ScreenshotConfiguration configuration, + Map> stepIgnores) + { + P parameters = createScreenshotParameters(); parameters.setShootingStrategy(configuration.getShootingStrategy()); parameters.setNativeFooterToCut(ensureValidCutSize(configuration.getNativeFooterToCut(), "native footer")); + + Map> ignores = new EnumMap<>(IgnoreStrategy.class); + + for (Map.Entry> ignoreStrategy : ignoreStrategies.entrySet()) + { + IgnoreStrategy cropStrategy = ignoreStrategy.getKey(); + Set ignore = Stream.concat( + getLocatorsStream(ignoreStrategy.getValue()), + getLocatorsStream(stepIgnores.get(cropStrategy))) + .collect(Collectors.toSet()); + ignores.put(cropStrategy, ignore); + } + parameters.setIgnoreStrategies(ignores); + return parameters; } + protected abstract P createScreenshotParameters(); + + private Stream getLocatorsStream(Set locatorsSet) + { + return Optional.ofNullable(locatorsSet).stream().flatMap(Collection::stream); + } + public void setShootingStrategy(String shootingStrategy) { this.shootingStrategy = shootingStrategy; @@ -67,4 +108,9 @@ public void setScreenshotConfigurations(PropertyMappedCollection screenshotCo { this.screenshotConfigurations = screenshotConfigurations; } + + public void setIgnoreStrategies(Map> ignoreStrategies) + { + this.ignoreStrategies = ignoreStrategies; + } } diff --git a/vividus-extension-selenium/src/main/java/org/vividus/ui/screenshot/ScreenshotConfiguration.java b/vividus-extension-selenium/src/main/java/org/vividus/ui/screenshot/ScreenshotConfiguration.java index c1b19ccac2..81df471fd5 100644 --- a/vividus-extension-selenium/src/main/java/org/vividus/ui/screenshot/ScreenshotConfiguration.java +++ b/vividus-extension-selenium/src/main/java/org/vividus/ui/screenshot/ScreenshotConfiguration.java @@ -17,11 +17,16 @@ package org.vividus.ui.screenshot; import java.util.Optional; +import java.util.Set; + +import org.vividus.ui.action.search.Locator; public class ScreenshotConfiguration { private int nativeFooterToCut; private Optional shootingStrategy = Optional.empty(); + private Set elementsToIgnore = Set.of(); + private Set areasToIgnore = Set.of(); public int getNativeFooterToCut() { @@ -42,4 +47,24 @@ public void setShootingStrategy(Optional shootingStrategy) { this.shootingStrategy = shootingStrategy; } + + public Set getElementsToIgnore() + { + return elementsToIgnore; + } + + public void setElementsToIgnore(Set elementsToIgnore) + { + this.elementsToIgnore = elementsToIgnore; + } + + public Set getAreasToIgnore() + { + return areasToIgnore; + } + + public void setAreasToIgnore(Set areasToIgnore) + { + this.areasToIgnore = areasToIgnore; + } } diff --git a/vividus-extension-selenium/src/main/java/org/vividus/ui/screenshot/ScreenshotParameters.java b/vividus-extension-selenium/src/main/java/org/vividus/ui/screenshot/ScreenshotParameters.java index 5bae2c0cf3..af5559cb73 100644 --- a/vividus-extension-selenium/src/main/java/org/vividus/ui/screenshot/ScreenshotParameters.java +++ b/vividus-extension-selenium/src/main/java/org/vividus/ui/screenshot/ScreenshotParameters.java @@ -16,12 +16,18 @@ package org.vividus.ui.screenshot; +import java.util.Map; import java.util.Optional; +import java.util.Set; + +import org.vividus.selenium.screenshot.IgnoreStrategy; +import org.vividus.ui.action.search.Locator; public class ScreenshotParameters { private int nativeFooterToCut; private Optional shootingStrategy = Optional.empty(); + private Map> ignoreStrategies; public int getNativeFooterToCut() { @@ -42,4 +48,14 @@ public void setShootingStrategy(Optional shootingStrategy) { this.shootingStrategy = shootingStrategy; } + + public Map> getIgnoreStrategies() + { + return ignoreStrategies; + } + + public void setIgnoreStrategies(Map> ignoreStrategies) + { + this.ignoreStrategies = ignoreStrategies; + } } diff --git a/vividus-extension-selenium/src/main/java/org/vividus/ui/screenshot/ScreenshotParametersFactory.java b/vividus-extension-selenium/src/main/java/org/vividus/ui/screenshot/ScreenshotParametersFactory.java index 2d864fba42..fd8e509247 100644 --- a/vividus-extension-selenium/src/main/java/org/vividus/ui/screenshot/ScreenshotParametersFactory.java +++ b/vividus-extension-selenium/src/main/java/org/vividus/ui/screenshot/ScreenshotParametersFactory.java @@ -16,9 +16,16 @@ package org.vividus.ui.screenshot; +import java.util.Map; import java.util.Optional; +import java.util.Set; + +import org.vividus.selenium.screenshot.IgnoreStrategy; +import org.vividus.ui.action.search.Locator; public interface ScreenshotParametersFactory { Optional create(Optional screenshotConfiguration); + + Optional create(Map> ignores); } diff --git a/vividus-extension-selenium/src/main/java/ru/yandex/qatools/ashot/shooting/ElementCroppingDecorator.java b/vividus-extension-selenium/src/main/java/ru/yandex/qatools/ashot/shooting/ElementCroppingDecorator.java new file mode 100644 index 0000000000..972071b4f6 --- /dev/null +++ b/vividus-extension-selenium/src/main/java/ru/yandex/qatools/ashot/shooting/ElementCroppingDecorator.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ru.yandex.qatools.ashot.shooting; + +import java.awt.image.BufferedImage; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.openqa.selenium.WebDriver; +import org.vividus.selenium.screenshot.IgnoreStrategy; +import org.vividus.selenium.screenshot.ScreenshotCropper; +import org.vividus.ui.action.search.Locator; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import ru.yandex.qatools.ashot.coordinates.Coords; + +public class ElementCroppingDecorator extends ShootingDecorator +{ + private static final long serialVersionUID = 1088965678314008274L; + + private final transient ScreenshotCropper screenshotCropper; + @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") + private final transient Map> ignoreStrategies; + + public ElementCroppingDecorator(ShootingStrategy shootingStrategy, ScreenshotCropper screenshotCropper, + Map> ignoreStrategies) + { + super(shootingStrategy); + this.screenshotCropper = screenshotCropper; + this.ignoreStrategies = ignoreStrategies; + } + + @Override + public BufferedImage getScreenshot(WebDriver webDriver) + { + BufferedImage image = getShootingStrategy().getScreenshot(webDriver); + return screenshotCropper.crop(image, Optional.empty(), ignoreStrategies, 0); + } + + @Override + public BufferedImage getScreenshot(WebDriver webDriver, Set coords) + { + Coords originalCoords = coords.iterator().next(); + Coords contextCoords = new Coords(originalCoords); + int contextY = contextCoords.y; + BufferedImage image = getShootingStrategy().getScreenshot(webDriver, coords); + Set preparedCoords = prepareCoords(Set.of(contextCoords)); + return screenshotCropper.crop(image, Optional.of(originalCoords), ignoreStrategies, + contextY - preparedCoords.iterator().next().y); + } + + @Override + public Set prepareCoords(Set coordsSet) + { + return getShootingStrategy().prepareCoords(coordsSet); + } +} diff --git a/vividus-extension-selenium/src/main/resources/vividus-extension/spring.xml b/vividus-extension-selenium/src/main/resources/vividus-extension/spring.xml index 0824cb89ce..7abe9d5445 100644 --- a/vividus-extension-selenium/src/main/resources/vividus-extension/spring.xml +++ b/vividus-extension-selenium/src/main/resources/vividus-extension/spring.xml @@ -9,6 +9,7 @@ http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd" default-lazy-init="true" profile="web,mobile_app"> + @@ -158,6 +159,15 @@ + + + + + + + + + diff --git a/vividus-plugin-web-app/src/test/java/org/vividus/jackson/databind/ui/web/LocatorDeserializerTests.java b/vividus-extension-selenium/src/test/java/org/vividus/jackson/databind/ui/LocatorDeserializerTests.java similarity index 97% rename from vividus-plugin-web-app/src/test/java/org/vividus/jackson/databind/ui/web/LocatorDeserializerTests.java rename to vividus-extension-selenium/src/test/java/org/vividus/jackson/databind/ui/LocatorDeserializerTests.java index 7ca4298d06..dc9120e700 100644 --- a/vividus-plugin-web-app/src/test/java/org/vividus/jackson/databind/ui/web/LocatorDeserializerTests.java +++ b/vividus-extension-selenium/src/test/java/org/vividus/jackson/databind/ui/LocatorDeserializerTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.vividus.jackson.databind.ui.web; +package org.vividus.jackson.databind.ui; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; diff --git a/vividus-extension-selenium/src/test/java/org/vividus/selenium/screenshot/AbstractAdjustingCoordsProviderTests.java b/vividus-extension-selenium/src/test/java/org/vividus/selenium/screenshot/AbstractAdjustingCoordsProviderTests.java deleted file mode 100644 index caa397fb36..0000000000 --- a/vividus-extension-selenium/src/test/java/org/vividus/selenium/screenshot/AbstractAdjustingCoordsProviderTests.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2019-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.vividus.selenium.screenshot; - -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.function.Supplier; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.openqa.selenium.WebElement; -import org.vividus.ui.context.IUiContext; - -import ru.yandex.qatools.ashot.coordinates.Coords; - -@ExtendWith(MockitoExtension.class) -class AbstractAdjustingCoordsProviderTests -{ - @Mock private IUiContext uiContext; - - @Test - void shouldAdjustCoordsToTheCurrentSearchContext() - { - WebElement contextElement = mock(WebElement.class); - Coords webElementCoords = new Coords(5, 15, 150, 30); - Coords searchContextCoords = new Coords(10, 10, 100, 50); - when(uiContext.getSearchContext()).thenReturn(contextElement); - Coords coords = new TestCoordsProvider(uiContext, - () -> searchContextCoords).adjustToSearchContext(webElementCoords); - assertAll(() -> assertEquals(0, coords.getX()), - () -> assertEquals(5, coords.getY()), - () -> assertEquals(100, coords.getWidth()), - () -> assertEquals(30, coords.getHeight())); - } - - @Test - void shouldReturnAsIsIfTheContextIsNotSet() - { - Coords webElementCoords = new Coords(5, 15, 150, 30); - assertSame(webElementCoords, new TestCoordsProvider(uiContext, () -> - { - throw new IllegalStateException("will not run"); - }).adjustToSearchContext(webElementCoords)); - } - - @Test - void shouldExposeUIContext() - { - assertSame(uiContext, new TestCoordsProvider(uiContext, null).getUiContext()); - } - - private static final class TestCoordsProvider extends AbstractAdjustingCoordsProvider - { - private static final long serialVersionUID = 6120548730279509334L; - - private final transient Supplier coordsProvider; - - private TestCoordsProvider(IUiContext uiContext, Supplier coordsProvider) - { - super(uiContext); - this.coordsProvider = coordsProvider; - } - - @Override - protected Coords getCoords(WebElement element) - { - return coordsProvider.get(); - } - } -} diff --git a/vividus-extension-selenium/src/test/java/org/vividus/selenium/screenshot/AbstractAshotFactoryTests.java b/vividus-extension-selenium/src/test/java/org/vividus/selenium/screenshot/AbstractAshotFactoryTests.java index c3f0ae0623..9b32130b53 100644 --- a/vividus-extension-selenium/src/test/java/org/vividus/selenium/screenshot/AbstractAshotFactoryTests.java +++ b/vividus-extension-selenium/src/test/java/org/vividus/selenium/screenshot/AbstractAshotFactoryTests.java @@ -28,20 +28,28 @@ import org.apache.commons.lang3.reflect.FieldUtils; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.vividus.selenium.screenshot.strategies.ScreenshotShootingStrategy; import org.vividus.ui.screenshot.ScreenshotParameters; import ru.yandex.qatools.ashot.AShot; import ru.yandex.qatools.ashot.shooting.CuttingDecorator; +import ru.yandex.qatools.ashot.shooting.ElementCroppingDecorator; import ru.yandex.qatools.ashot.shooting.ShootingStrategies; import ru.yandex.qatools.ashot.shooting.ShootingStrategy; +@ExtendWith(MockitoExtension.class) class AbstractAshotFactoryTests { private static final String DEFAULT = "default"; - private final TestAshotFactory factory = new TestAshotFactory(); + + @Mock private ScreenshotCropper screenshotCropper; + @InjectMocks private TestAshotFactory factory; @Test void shouldProvideScalingBaseStrategy() throws IllegalAccessException @@ -93,11 +101,24 @@ void shouldDecorateWithCutting(int headerToCut, int footerToCut) instanceOf(CuttingDecorator.class)); } + @Test + void shouldDecorateWithCropping() + { + var strategy = mock(ShootingStrategy.class); + assertThat(factory.decorateWithCropping(strategy, Optional.empty()), + instanceOf(ElementCroppingDecorator.class)); + } + private static final class TestAshotFactory extends AbstractAshotFactory { private static final double DPR = 101d; private ScreenshotShootingStrategy strategy; + TestAshotFactory(ScreenshotCropper screenshotCropper) + { + super(screenshotCropper); + } + @Override public AShot create(Optional screenshotParameters) { diff --git a/vividus-extension-selenium/src/test/java/org/vividus/selenium/screenshot/ScreenshotCropperTests.java b/vividus-extension-selenium/src/test/java/org/vividus/selenium/screenshot/ScreenshotCropperTests.java new file mode 100644 index 0000000000..eb4b7c1160 --- /dev/null +++ b/vividus-extension-selenium/src/test/java/org/vividus/selenium/screenshot/ScreenshotCropperTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.vividus.selenium.screenshot; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import javax.imageio.ImageIO; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.vividus.selenium.IWebDriverProvider; +import org.vividus.ui.action.ISearchActions; +import org.vividus.ui.action.search.Locator; +import org.vividus.util.ResourceUtils; + +import ru.yandex.qatools.ashot.coordinates.Coords; +import ru.yandex.qatools.ashot.coordinates.CoordsProvider; +import ru.yandex.qatools.ashot.util.ImageTool; + +@ExtendWith(MockitoExtension.class) +class ScreenshotCropperTests +{ + private static final String ORIGINAL = "original"; + + @Mock private Locator emptyLocator; + @Mock private Locator elementLocator; + @Mock private Locator areaLocator; + @Mock private WebDriver webDriver; + + @Mock private ScreenshotDebugger screenshotDebugger; + @Mock private ISearchActions searchActions; + @Mock private CoordsProvider coordsProvider; + @Mock private IWebDriverProvider webDriverProvider; + @InjectMocks private ScreenshotCropper cropper; + + @Test + void shouldCropElementsFromImage() throws IOException + { + when(webDriverProvider.get()).thenReturn(webDriver); + WebElement element = mock(WebElement.class); + when(searchActions.findElements(elementLocator)).thenReturn(List.of(element)); + when(searchActions.findElements(emptyLocator)).thenReturn(List.of()); + when(coordsProvider.ofElements(webDriver, List.of(element))).thenReturn(Set.of(new Coords(704, 89, 272, 201))); + WebElement area = mock(WebElement.class); + when(searchActions.findElements(areaLocator)).thenReturn(List.of(area)); + when(coordsProvider.ofElements(webDriver, List.of(area))).thenReturn(Set.of(new Coords(270, 311, 1139, 52))); + + BufferedImage originalImage = loadImage(ORIGINAL); + BufferedImage actual = cropper.crop(originalImage, Optional.empty(), Map.of( + IgnoreStrategy.ELEMENT, Set.of(elementLocator, emptyLocator), + IgnoreStrategy.AREA, Set.of(areaLocator) + ), 0); + + verifyScreenshot(actual); + } + + @Test + void shouldCropNothingIfNoElementsWereFound() throws IOException + { + when(webDriverProvider.get()).thenReturn(webDriver); + when(searchActions.findElements(emptyLocator)).thenReturn(List.of()); + + BufferedImage originalImage = loadImage(ORIGINAL); + BufferedImage actual = cropper.crop(originalImage, Optional.empty(), Map.of( + IgnoreStrategy.ELEMENT, Set.of(emptyLocator) + ), 0); + + assertThat(actual, ImageTool.equalImage(originalImage)); + } + + @Test + void shouldReturnCoordsProvider() + { + assertEquals(coordsProvider, cropper.getCoordsProvider()); + } + + @Test + void shouldReturnWebDriverProvider() + { + assertEquals(webDriverProvider, cropper.getWebDriverProvider()); + } + + private void verifyScreenshot(BufferedImage actual) throws IOException + { + BufferedImage afterCropping = loadImage("after_cropping"); + assertThat(actual, ImageTool.equalImage(afterCropping)); + verify(searchActions).findElements(emptyLocator); + verify(searchActions).findElements(elementLocator); + verify(searchActions).findElements(areaLocator); + BufferedImage elementCropped = loadImage("element_cropped"); + verify(screenshotDebugger).debug(eq(ScreenshotCropper.class), eq("cropped_by_ELEMENT"), + equalTo(elementCropped)); + verify(screenshotDebugger).debug(eq(ScreenshotCropper.class), eq("cropped_by_AREA"), + equalTo(afterCropping)); + } + + private BufferedImage loadImage(String fileName) throws IOException + { + return ImageIO.read(ResourceUtils.loadFile(ScreenshotCropperTests.class, fileName + ".png")); + } + + private BufferedImage equalTo(BufferedImage expected) + { + return argThat(actual -> ImageTool.equalImage(expected).matches(actual)); + } +} diff --git a/vividus-extension-selenium/src/test/java/org/vividus/ui/screenshot/AbstractScreenshotParametersFactoryTests.java b/vividus-extension-selenium/src/test/java/org/vividus/ui/screenshot/AbstractScreenshotParametersFactoryTests.java index 38fa7834f9..6190024fbe 100644 --- a/vividus-extension-selenium/src/test/java/org/vividus/ui/screenshot/AbstractScreenshotParametersFactoryTests.java +++ b/vividus-extension-selenium/src/test/java/org/vividus/ui/screenshot/AbstractScreenshotParametersFactoryTests.java @@ -23,10 +23,14 @@ import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; +import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.BinaryOperator; import org.junit.jupiter.api.Test; +import org.vividus.selenium.screenshot.IgnoreStrategy; +import org.vividus.ui.action.search.Locator; import org.vividus.util.property.PropertyMappedCollection; class AbstractScreenshotParametersFactoryTests @@ -98,15 +102,31 @@ void shouldFailOnInvalidCutSize() @Test void shouldCreateConfigurationWithBaseParameters() { + Locator stepElementLocator = mock(Locator.class); + Locator stepAreaLocator = mock(Locator.class); + ScreenshotConfiguration parameters = new ScreenshotConfiguration(); + parameters.setElementsToIgnore(Set.of(stepElementLocator)); + parameters.setAreasToIgnore(Set.of(stepAreaLocator)); parameters.setShootingStrategy(Optional.of(DEFAULT)); parameters.setNativeFooterToCut(1); - ScreenshotParameters configuration = factory.createWithBaseConfiguration(parameters, - ScreenshotParameters::new); + Locator commonElementLocator = mock(Locator.class); + Locator commonAreaLocator = mock(Locator.class); + + factory.setIgnoreStrategies(Map.of( + IgnoreStrategy.ELEMENT, Set.of(commonElementLocator), + IgnoreStrategy.AREA, Set.of(commonAreaLocator) + )); + + ScreenshotParameters configuration = factory.createWithBaseConfiguration(parameters); assertEquals(Optional.of(DEFAULT), configuration.getShootingStrategy()); assertEquals(1, configuration.getNativeFooterToCut()); + assertEquals(Map.of( + IgnoreStrategy.ELEMENT, Set.of(stepElementLocator, commonElementLocator), + IgnoreStrategy.AREA, Set.of(stepAreaLocator, commonAreaLocator) + ), configuration.getIgnoreStrategies()); } private ScreenshotConfiguration createParametersWith(int nativeFooterToCut) @@ -117,12 +137,24 @@ private ScreenshotConfiguration createParametersWith(int nativeFooterToCut) } private static final class TestScreenshotParametersFactory - extends AbstractScreenshotParametersFactory + extends AbstractScreenshotParametersFactory { @Override public Optional create(Optional screenshotConfiguration) { return Optional.empty(); } + + @Override + public Optional create(Map> ignores) + { + return Optional.empty(); + } + + @Override + protected ScreenshotParameters createScreenshotParameters() + { + return new ScreenshotParameters(); + } } } diff --git a/vividus-extension-selenium/src/test/java/org/vividus/ui/screenshot/ScreenshotConfigurationTests.java b/vividus-extension-selenium/src/test/java/org/vividus/ui/screenshot/ScreenshotConfigurationTests.java new file mode 100644 index 0000000000..e8570b70ff --- /dev/null +++ b/vividus-extension-selenium/src/test/java/org/vividus/ui/screenshot/ScreenshotConfigurationTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.vividus.ui.screenshot; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Set; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.vividus.ui.action.search.Locator; + +@ExtendWith(MockitoExtension.class) +class ScreenshotConfigurationTests +{ + @Mock private Locator locator; + + @Test + void shouldSetAndGetElementToIgnore() + { + ScreenshotConfiguration config = new ScreenshotConfiguration(); + assertEquals(Set.of(), config.getElementsToIgnore()); + config.setElementsToIgnore(Set.of(locator)); + assertEquals(Set.of(locator), config.getElementsToIgnore()); + } + + @Test + void shouldSetAndGetAreaToIgnore() + { + ScreenshotConfiguration config = new ScreenshotConfiguration(); + assertEquals(Set.of(), config.getAreasToIgnore()); + config.setAreasToIgnore(Set.of(locator)); + assertEquals(Set.of(locator), config.getAreasToIgnore()); + } +} diff --git a/vividus-extension-selenium/src/test/java/org/vividus/ui/screenshot/ScreenshotParametersTests.java b/vividus-extension-selenium/src/test/java/org/vividus/ui/screenshot/ScreenshotParametersTests.java index 0b6e79d79a..00f0627d3d 100644 --- a/vividus-extension-selenium/src/test/java/org/vividus/ui/screenshot/ScreenshotParametersTests.java +++ b/vividus-extension-selenium/src/test/java/org/vividus/ui/screenshot/ScreenshotParametersTests.java @@ -17,11 +17,17 @@ package org.vividus.ui.screenshot; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; +import java.util.Map; import java.util.Optional; +import java.util.Set; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.vividus.selenium.screenshot.IgnoreStrategy; +import org.vividus.ui.action.search.Locator; class ScreenshotParametersTests { @@ -33,4 +39,14 @@ void shouldProvideDefalutValues() () -> assertEquals(Optional.empty(), screenshotParameters.getShootingStrategy())); } + + @Test + void shouldGetAndSetIgnoreStrategies() + { + ScreenshotParameters screenshotParameters = new ScreenshotParameters(); + assertNull(screenshotParameters.getIgnoreStrategies()); + Locator locator = mock(Locator.class); + screenshotParameters.setIgnoreStrategies(Map.of(IgnoreStrategy.ELEMENT, Set.of(locator))); + assertEquals(Map.of(IgnoreStrategy.ELEMENT, Set.of(locator)), screenshotParameters.getIgnoreStrategies()); + } } diff --git a/vividus-extension-selenium/src/test/java/ru/yandex/qatools/ashot/shooting/ElementCroppingDecoratorTests.java b/vividus-extension-selenium/src/test/java/ru/yandex/qatools/ashot/shooting/ElementCroppingDecoratorTests.java new file mode 100644 index 0000000000..ff18f066fe --- /dev/null +++ b/vividus-extension-selenium/src/test/java/ru/yandex/qatools/ashot/shooting/ElementCroppingDecoratorTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ru.yandex.qatools.ashot.shooting; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +import java.awt.image.BufferedImage; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openqa.selenium.WebDriver; +import org.vividus.selenium.screenshot.ScreenshotCropper; + +import ru.yandex.qatools.ashot.coordinates.Coords; + +@ExtendWith(MockitoExtension.class) +class ElementCroppingDecoratorTests +{ + @Mock private ShootingStrategy shootingStrategy; + @Mock private BufferedImage image; + @Mock private WebDriver webDriver; + @Mock private ScreenshotCropper screenshotCropper; + + private ElementCroppingDecorator decorator; + + @BeforeEach + void init() + { + decorator = new ElementCroppingDecorator(shootingStrategy, screenshotCropper, Map.of()); + } + + @Test + void shouldGetScreenshot() + { + when(shootingStrategy.getScreenshot(webDriver)).thenReturn(image); + when(screenshotCropper.crop(image, Optional.empty(), Map.of(), 0)).thenReturn(image); + + BufferedImage result = decorator.getScreenshot(webDriver); + assertEquals(image, result); + } + + @Test + void shouldPrepareCoords() + { + Coords coords = new Coords(1, 1, 1, 1); + when(shootingStrategy.prepareCoords(Set.of(coords))).thenReturn(Set.of(coords)); + assertEquals(Set.of(coords), decorator.prepareCoords(Set.of(coords))); + } + + @Test + void shouldReturnImageOfContextElement() + { + Coords contextCoords = new Coords(16, 2331, 1888, 515); + Coords preparedCoords = new Coords(16, 206, 1888, 515); + + when(shootingStrategy.getScreenshot(webDriver, Set.of(contextCoords))).thenReturn(image); + when(shootingStrategy.prepareCoords(Set.of(contextCoords))).thenReturn(Set.of(preparedCoords)); + when(screenshotCropper.crop(image, Optional.of(contextCoords), Map.of(), 2125)).thenReturn(image); + + decorator = new ElementCroppingDecorator(shootingStrategy, screenshotCropper, Map.of()); + BufferedImage screenshot = decorator.getScreenshot(webDriver, Set.of(contextCoords)); + assertEquals(image, screenshot); + } +} diff --git a/vividus-extension-selenium/src/test/resources/org/vividus/selenium/screenshot/after_cropping.png b/vividus-extension-selenium/src/test/resources/org/vividus/selenium/screenshot/after_cropping.png new file mode 100644 index 0000000000000000000000000000000000000000..d839d5ca25482029cc423d1bc581e2aca8431d47 GIT binary patch literal 25914 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 literal 0 HcmV?d00001 diff --git a/vividus-extension-selenium/src/test/resources/org/vividus/selenium/screenshot/element_cropped.png b/vividus-extension-selenium/src/test/resources/org/vividus/selenium/screenshot/element_cropped.png new file mode 100644 index 0000000000000000000000000000000000000000..5baa4b441283e9bacb1732abe5f52a3ef332ecba GIT binary patch literal 28908 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 E0Q1ri6#xJL literal 0 HcmV?d00001 diff --git a/vividus-extension-visual-testing/src/main/java/org/vividus/visual/AbstractVisualCheckFactory.java b/vividus-extension-visual-testing/src/main/java/org/vividus/visual/AbstractVisualCheckFactory.java deleted file mode 100644 index 5c2f7540b8..0000000000 --- a/vividus-extension-visual-testing/src/main/java/org/vividus/visual/AbstractVisualCheckFactory.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2019-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.vividus.visual; - -import java.util.Map; -import java.util.Optional; - -import javax.inject.Inject; - -import org.vividus.ui.screenshot.ScreenshotConfiguration; -import org.vividus.ui.screenshot.ScreenshotParametersFactory; -import org.vividus.visual.model.AbstractVisualCheck; -import org.vividus.visual.screenshot.IScreenshotIndexer; - -public abstract class AbstractVisualCheckFactory -{ - private Map indexers; - private Optional screenshotIndexer; - - private final ScreenshotParametersFactory screenshotParametersFactory; - - protected AbstractVisualCheckFactory( - ScreenshotParametersFactory screenshotParametersFactory) - { - this.screenshotParametersFactory = screenshotParametersFactory; - } - - protected String createIndexedBaseline(String baselineName) - { - return screenshotIndexer.map(indexers::get) - .map(indexer -> indexer.index(baselineName)) - .orElse(baselineName); - } - - protected void withScreenshotConfiguration(T check, Optional configuration) - { - check.setScreenshotParameters(screenshotParametersFactory.create(configuration)); - } - - public void setScreenshotIndexer(Optional screenshotIndexer) - { - this.screenshotIndexer = screenshotIndexer; - } - - @Inject - public void setIndexers(Map indexers) - { - this.indexers = indexers; - } -} diff --git a/vividus-extension-visual-testing/src/main/java/org/vividus/visual/model/AbstractVisualCheck.java b/vividus-extension-visual-testing/src/main/java/org/vividus/visual/model/AbstractVisualCheck.java index a7fc1d2f2f..dbf76a62d8 100644 --- a/vividus-extension-visual-testing/src/main/java/org/vividus/visual/model/AbstractVisualCheck.java +++ b/vividus-extension-visual-testing/src/main/java/org/vividus/visual/model/AbstractVisualCheck.java @@ -16,20 +16,15 @@ package org.vividus.visual.model; -import java.util.Map; import java.util.Optional; -import java.util.Set; import org.openqa.selenium.SearchContext; -import org.vividus.ui.action.search.Locator; import org.vividus.ui.screenshot.ScreenshotParameters; -import org.vividus.visual.screenshot.IgnoreStrategy; public abstract class AbstractVisualCheck { private String baselineName; private VisualActionType action; - private Map> elementsToIgnore = Map.of(); private Optional screenshotParameters = Optional.empty(); private SearchContext searchContext; @@ -49,16 +44,6 @@ public String getBaselineName() return baselineName; } - public Map> getElementsToIgnore() - { - return elementsToIgnore; - } - - public void setElementsToIgnore(Map> elementsToIgnore) - { - this.elementsToIgnore = elementsToIgnore; - } - public VisualActionType getAction() { return action; diff --git a/vividus-extension-visual-testing/src/main/java/org/vividus/visual/screenshot/AshotScreenshotProvider.java b/vividus-extension-visual-testing/src/main/java/org/vividus/visual/screenshot/AshotScreenshotProvider.java deleted file mode 100644 index c72d8e74ce..0000000000 --- a/vividus-extension-visual-testing/src/main/java/org/vividus/visual/screenshot/AshotScreenshotProvider.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2019-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.vividus.visual.screenshot; - -import java.awt.image.BufferedImage; -import java.util.Collection; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.vividus.selenium.IWebDriverProvider; -import org.vividus.selenium.screenshot.AshotScreenshotTaker; -import org.vividus.selenium.screenshot.ScreenshotDebugger; -import org.vividus.ui.action.ISearchActions; -import org.vividus.ui.action.search.Locator; -import org.vividus.ui.screenshot.ScreenshotParameters; -import org.vividus.visual.model.AbstractVisualCheck; - -import ru.yandex.qatools.ashot.Screenshot; -import ru.yandex.qatools.ashot.coordinates.Coords; -import ru.yandex.qatools.ashot.coordinates.CoordsProvider; - -public class AshotScreenshotProvider implements ScreenshotProvider -{ - private final AshotScreenshotTaker ashotScreenshotTaker; - private final ISearchActions searchActions; - private final ScreenshotDebugger screenshotDebugger; - private final CoordsProvider coordsProvider; - private final IWebDriverProvider webDriverProvider; - - private Map> ignoreStrategies; - - @SuppressWarnings({"rawtypes", "unchecked"}) - public AshotScreenshotProvider(AshotScreenshotTaker ashotScreenshotTaker, - ISearchActions searchActions, ScreenshotDebugger screenshotDebugger, CoordsProvider coordsProvider, - IWebDriverProvider webDriverProvider) - { - this.ashotScreenshotTaker = ashotScreenshotTaker; - this.searchActions = searchActions; - this.screenshotDebugger = screenshotDebugger; - this.coordsProvider = coordsProvider; - this.webDriverProvider = webDriverProvider; - } - - @Override - public Screenshot take(AbstractVisualCheck visualCheck) - { - Screenshot screenshot = ashotScreenshotTaker.takeAshotScreenshot(visualCheck.getSearchContext(), - visualCheck.getScreenshotParameters()); - BufferedImage original = screenshot.getImage(); - Map> stepLevelElementsToIgnore = visualCheck.getElementsToIgnore(); - for (Map.Entry> strategy : ignoreStrategies.entrySet()) - { - IgnoreStrategy cropStrategy = strategy.getKey(); - Set ignore = Stream.concat( - getLocatorsStream(strategy.getValue()), - getLocatorsStream(stepLevelElementsToIgnore.get(cropStrategy))) - .distinct() - .map(searchActions::findElements) - .flatMap(Collection::stream) - .map(e -> coordsProvider.ofElement(webDriverProvider.get(), e)) - .collect(Collectors.toSet()); - if (ignore.isEmpty()) - { - continue; - } - original = cropStrategy.crop(original, ignore); - screenshotDebugger.debug(this.getClass(), "cropped_by_" + cropStrategy, original); - } - screenshot.setImage(original); - return screenshot; - } - - private Stream getLocatorsStream(Set locatorsSet) - { - return Optional.ofNullable(locatorsSet).stream().flatMap(Collection::stream); - } - - public void setIgnoreStrategies(Map> ignoreStrategies) - { - this.ignoreStrategies = ignoreStrategies; - } -} diff --git a/vividus-extension-visual-testing/src/main/java/org/vividus/visual/screenshot/ScreenshotProvider.java b/vividus-extension-visual-testing/src/main/java/org/vividus/visual/screenshot/BaselineIndexer.java similarity index 51% rename from vividus-extension-visual-testing/src/main/java/org/vividus/visual/screenshot/ScreenshotProvider.java rename to vividus-extension-visual-testing/src/main/java/org/vividus/visual/screenshot/BaselineIndexer.java index 7b842a0c3c..16dbce7738 100644 --- a/vividus-extension-visual-testing/src/main/java/org/vividus/visual/screenshot/ScreenshotProvider.java +++ b/vividus-extension-visual-testing/src/main/java/org/vividus/visual/screenshot/BaselineIndexer.java @@ -16,11 +16,24 @@ package org.vividus.visual.screenshot; -import org.vividus.visual.model.AbstractVisualCheck; +import java.util.Map; +import java.util.Optional; -import ru.yandex.qatools.ashot.Screenshot; - -public interface ScreenshotProvider +public class BaselineIndexer { - Screenshot take(AbstractVisualCheck visualCheck); + private final Map indexers; + private final Optional screenshotIndexer; + + public BaselineIndexer(Map indexers, Optional screenshotIndexer) + { + this.indexers = indexers; + this.screenshotIndexer = screenshotIndexer; + } + + public String createIndexedBaseline(String baselineName) + { + return screenshotIndexer.map(indexers::get) + .map(indexer -> indexer.index(baselineName)) + .orElse(baselineName); + } } diff --git a/vividus-extension-visual-testing/src/main/java/org/vividus/visual/steps/AbstractVisualSteps.java b/vividus-extension-visual-testing/src/main/java/org/vividus/visual/steps/AbstractVisualSteps.java index afe0738dbe..f494ad5db0 100644 --- a/vividus-extension-visual-testing/src/main/java/org/vividus/visual/steps/AbstractVisualSteps.java +++ b/vividus-extension-visual-testing/src/main/java/org/vividus/visual/steps/AbstractVisualSteps.java @@ -17,12 +17,19 @@ package org.vividus.visual.steps; import java.util.Map; +import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; +import org.apache.commons.lang3.Validate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.vividus.reporter.event.IAttachmentPublisher; +import org.vividus.selenium.screenshot.IgnoreStrategy; import org.vividus.softassert.ISoftAssert; +import org.vividus.ui.action.search.Locator; import org.vividus.ui.context.IUiContext; +import org.vividus.ui.screenshot.ScreenshotConfiguration; import org.vividus.ui.screenshot.ScreenshotPrecondtionMismatchException; import org.vividus.visual.model.AbstractVisualCheck; import org.vividus.visual.model.VisualActionType; @@ -30,6 +37,8 @@ public abstract class AbstractVisualSteps { + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractVisualSteps.class); + private final IUiContext uiContext; private final IAttachmentPublisher attachmentPublisher; private final ISoftAssert softAssert; @@ -42,9 +51,8 @@ protected AbstractVisualSteps(IUiContext uiContext, IAttachmentPublisher attachm this.softAssert = softAssert; } - protected void execute( - Function checkResultProvider, - Supplier visualCheckFactory, String templateName) + protected void execute(Supplier visualCheckFactory, + Function checkResultProvider, String templateName) { uiContext.getOptionalSearchContext().ifPresent(searchContext -> { @@ -74,6 +82,31 @@ protected void verifyResult(VisualCheckResult result) softAssert.assertTrue("Visual check passed", passed); } + protected void patchIgnores(String sourceKey, ScreenshotConfiguration screenshotConfiguration, + Map> ignores) + { + Set elementsToIgnore = getIgnoresFromOneOf(screenshotConfiguration.getElementsToIgnore(), sourceKey, + ignores.get(IgnoreStrategy.ELEMENT)); + screenshotConfiguration.setElementsToIgnore(elementsToIgnore); + + Set areasToIgnore = getIgnoresFromOneOf(screenshotConfiguration.getAreasToIgnore(), sourceKey, + ignores.get(IgnoreStrategy.AREA)); + screenshotConfiguration.setAreasToIgnore(areasToIgnore); + } + + private Set getIgnoresFromOneOf(Set configIgnores, String sourceKey, Set source) + { + if (!source.isEmpty()) + { + Validate.isTrue(configIgnores.isEmpty(), "The elements and areas to ignore must be passed " + + "either through screenshot configuration or %s", sourceKey); + LOGGER.atWarn().addArgument(sourceKey).log("The passing of elements and areas to ignore through {}" + + " is deprecated, please use screenshot configuration instead"); + return source; + } + return configIgnores; + } + protected ISoftAssert getSoftAssert() { return softAssert; diff --git a/vividus-extension-visual-testing/src/main/resources/vividus-extension/spring.xml b/vividus-extension-visual-testing/src/main/resources/vividus-extension/spring.xml index e9fb810833..2243c05a00 100644 --- a/vividus-extension-visual-testing/src/main/resources/vividus-extension/spring.xml +++ b/vividus-extension-visual-testing/src/main/resources/vividus-extension/spring.xml @@ -7,21 +7,11 @@ default-lazy-init="true"> - - - - - - - - - - - - + + + - diff --git a/vividus-extension-visual-testing/src/test/java/org/vividus/visual/VisualCheckFactoryTests.java b/vividus-extension-visual-testing/src/test/java/org/vividus/visual/VisualCheckFactoryTests.java deleted file mode 100644 index eb0c78f70b..0000000000 --- a/vividus-extension-visual-testing/src/test/java/org/vividus/visual/VisualCheckFactoryTests.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2019-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.vividus.visual; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.Map; -import java.util.Optional; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.vividus.ui.screenshot.ScreenshotConfiguration; -import org.vividus.ui.screenshot.ScreenshotParameters; -import org.vividus.ui.screenshot.ScreenshotParametersFactory; -import org.vividus.visual.model.AbstractVisualCheck; -import org.vividus.visual.screenshot.IScreenshotIndexer; - -@ExtendWith(MockitoExtension.class) -class VisualCheckFactoryTests -{ - private static final String BASELINE = "baseline"; - private static final String INDEXER = "indexer"; - - private @Mock ScreenshotParametersFactory screenshotParametersFactory; - - @InjectMocks - private VisualCheckFactory visualCheckFactory; - - @Test - void shouldReturnSameBaselineIfIndexerNameNotSet() - { - visualCheckFactory.setScreenshotIndexer(Optional.empty()); - visualCheckFactory.setIndexers(Map.of()); - assertEquals(BASELINE, visualCheckFactory.createIndexedBaseline(BASELINE)); - } - - @Test - void shouldReturnSameBaselineIfIndexerNameSetButItDoesntExist() - { - visualCheckFactory.setScreenshotIndexer(Optional.of(INDEXER)); - visualCheckFactory.setIndexers(Map.of()); - assertEquals(BASELINE, visualCheckFactory.createIndexedBaseline(BASELINE)); - } - - @Test - void shouldModifyBaselineName() - { - visualCheckFactory.setScreenshotIndexer(Optional.of(INDEXER)); - var indexer = mock(IScreenshotIndexer.class); - visualCheckFactory.setIndexers(Map.of(INDEXER, indexer)); - var indexedBaseline = "baseline-1"; - when(indexer.index(BASELINE)).thenReturn(indexedBaseline); - assertEquals(indexedBaseline, visualCheckFactory.createIndexedBaseline(BASELINE)); - } - - @Test - void shouldSetScreenshotConfiguration() - { - var visualCheck = mock(AbstractVisualCheck.class); - var parameters = Optional.of(mock(ScreenshotParameters.class)); - when(screenshotParametersFactory.create(Optional.empty())).thenReturn(parameters); - visualCheckFactory.withScreenshotConfiguration(visualCheck, Optional.empty()); - verify(visualCheck).setScreenshotParameters(parameters); - } - - private static final class VisualCheckFactory extends AbstractVisualCheckFactory - { - protected VisualCheckFactory(ScreenshotParametersFactory screenshotParametersFactory) - { - super(screenshotParametersFactory); - } - } -} diff --git a/vividus-extension-visual-testing/src/test/java/org/vividus/visual/model/VisualCheckTests.java b/vividus-extension-visual-testing/src/test/java/org/vividus/visual/model/VisualCheckTests.java index f56cced8f7..ba850118d6 100644 --- a/vividus-extension-visual-testing/src/test/java/org/vividus/visual/model/VisualCheckTests.java +++ b/vividus-extension-visual-testing/src/test/java/org/vividus/visual/model/VisualCheckTests.java @@ -19,7 +19,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; -import java.util.Map; import java.util.Optional; import org.junit.jupiter.api.Test; @@ -31,7 +30,6 @@ class VisualCheckTests void shouldUseCorrectDefaultValues() { var visualCheck = new AbstractVisualCheck() { }; - assertEquals(Map.of(), visualCheck.getElementsToIgnore()); assertEquals(Optional.empty(), visualCheck.getScreenshotParameters()); var parameters = Optional.of(mock(ScreenshotParameters.class)); visualCheck.setScreenshotParameters(parameters); diff --git a/vividus-extension-visual-testing/src/test/java/org/vividus/visual/screenshot/AshotScreenshotProviderTests.java b/vividus-extension-visual-testing/src/test/java/org/vividus/visual/screenshot/AshotScreenshotProviderTests.java deleted file mode 100644 index 9d84cc286d..0000000000 --- a/vividus-extension-visual-testing/src/test/java/org/vividus/visual/screenshot/AshotScreenshotProviderTests.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright 2019-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.vividus.visual.screenshot; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; - -import java.awt.image.BufferedImage; -import java.io.IOException; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; - -import javax.imageio.ImageIO; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InOrder; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; -import org.openqa.selenium.SearchContext; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.vividus.selenium.IWebDriverProvider; -import org.vividus.selenium.screenshot.AshotScreenshotTaker; -import org.vividus.selenium.screenshot.ScreenshotDebugger; -import org.vividus.ui.action.ISearchActions; -import org.vividus.ui.action.search.Locator; -import org.vividus.util.ResourceUtils; -import org.vividus.visual.model.AbstractVisualCheck; -import org.vividus.visual.model.VisualActionType; - -import ru.yandex.qatools.ashot.Screenshot; -import ru.yandex.qatools.ashot.coordinates.Coords; -import ru.yandex.qatools.ashot.coordinates.CoordsProvider; -import ru.yandex.qatools.ashot.util.ImageTool; - -@ExtendWith(MockitoExtension.class) -class AshotScreenshotProviderTests -{ - private static final String BASELINE = "baseline"; - - @Mock - private Locator aLocator; - @Mock - private Locator bLocator; - @Mock - private Locator elementLocator; - @Mock - private Locator areaLocator; - - @Mock - private AshotScreenshotTaker screenshotTaker; - @Mock(lenient = true) - private ISearchActions searchActions; - @Mock - private ScreenshotDebugger screenshotDebugger; - @Mock - private CoordsProvider coordsProvider; - @Mock - private IWebDriverProvider webDriverProvider; - - @InjectMocks - private AshotScreenshotProvider screenshotProvider; - - private static Map> createMap(IgnoreStrategy key1, - Set value1, IgnoreStrategy key2, Set value2) - { - Map> map = new LinkedHashMap<>(2); - map.put(key1, value1); - map.put(key2, value2); - return map; - } - - @Test - void shouldTakeScreenshot() - { - SearchContext searchContext = mock(SearchContext.class); - var visualCheck = mockSearchContext(searchContext); - Screenshot screenshot = mock(Screenshot.class); - when(searchActions.findElements(aLocator)).thenReturn(List.of()); - when(screenshotTaker.takeAshotScreenshot(searchContext, Optional.empty())).thenReturn(screenshot); - screenshotProvider.setIgnoreStrategies(Map.of(IgnoreStrategy.AREA, Set.of(aLocator))); - assertSame(screenshot, screenshotProvider.take(visualCheck)); - verifyNoInteractions(screenshotDebugger); - } - - private AbstractVisualCheck mockSearchContext(SearchContext searchContext) - { - var visualCheck = new AbstractVisualCheck(BASELINE, VisualActionType.ESTABLISH) { }; - visualCheck.setSearchContext(searchContext); - return visualCheck; - } - - @Test - void shouldTakeScreenshotAndProcessIgnoredElements() throws IOException - { - Map> strategies = createMap(IgnoreStrategy.ELEMENT, - Set.of(elementLocator, aLocator), IgnoreStrategy.AREA, Set.of(bLocator)); - Map> stepLevelStrategies = Map.of(IgnoreStrategy.ELEMENT, Set.of(aLocator), - IgnoreStrategy.AREA, Set.of(bLocator, areaLocator)); - - SearchContext searchContext = mock(SearchContext.class); - var visualCheck = mockSearchContext(searchContext); - visualCheck.setElementsToIgnore(stepLevelStrategies); - screenshotProvider.setIgnoreStrategies(strategies); - Screenshot screenshot = new Screenshot(loadImage("original")); - WebDriver driver = mock(WebDriver.class); - when(webDriverProvider.get()).thenReturn(driver); - WebElement element = mock(WebElement.class); - when(searchActions.findElements(elementLocator)).thenReturn(List.of(element)); - when(coordsProvider.ofElement(driver, element)).thenReturn(new Coords(704, 89, 272, 201)); - WebElement area = mock(WebElement.class); - when(searchActions.findElements(areaLocator)).thenReturn(List.of(area)); - when(coordsProvider.ofElement(driver, area)).thenReturn(new Coords(270, 311, 1139, 52)); - when(screenshotTaker.takeAshotScreenshot(searchContext, Optional.empty())).thenReturn(screenshot); - - Screenshot actual = screenshotProvider.take(visualCheck); - - verifyScreenshot(screenshot, actual); - } - - private void verifyScreenshot(Screenshot screenshot, Screenshot actual) throws IOException - { - assertSame(actual, screenshot); - BufferedImage afterCropping = loadImage("after_cropping"); - assertThat(actual.getImage(), ImageTool.equalImage(afterCropping)); - verify(searchActions).findElements(aLocator); - verify(searchActions).findElements(bLocator); - verify(searchActions).findElements(elementLocator); - verify(searchActions).findElements(areaLocator); - BufferedImage elementCropped = loadImage("element_cropped"); - InOrder ordered = Mockito.inOrder(screenshotDebugger); - ordered.verify(screenshotDebugger).debug(eq(AshotScreenshotProvider.class), eq("cropped_by_ELEMENT"), - equalTo(elementCropped)); - ordered.verify(screenshotDebugger).debug(eq(AshotScreenshotProvider.class), eq("cropped_by_AREA"), - equalTo(afterCropping)); - } - - private BufferedImage equalTo(BufferedImage expected) - { - return argThat(actual -> ImageTool.equalImage(expected).matches(actual)); - } - - private BufferedImage loadImage(String fileName) throws IOException - { - return ImageIO.read(ResourceUtils.loadFile(AshotScreenshotProviderTests.class, "/" + fileName + ".png")); - } -} diff --git a/vividus-extension-visual-testing/src/test/java/org/vividus/visual/screenshot/BaselineIndexerTests.java b/vividus-extension-visual-testing/src/test/java/org/vividus/visual/screenshot/BaselineIndexerTests.java new file mode 100644 index 0000000000..d5a23db438 --- /dev/null +++ b/vividus-extension-visual-testing/src/test/java/org/vividus/visual/screenshot/BaselineIndexerTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.vividus.visual.screenshot; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Map; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +class BaselineIndexerTests +{ + private static final String BASELINE = "baseline"; + private static final String INDEXER = "indexer"; + + private BaselineIndexer baselineIndexer; + + @Test + void shouldReturnSameBaselineIfIndexerNameNotSet() + { + baselineIndexer = new BaselineIndexer(Map.of(), Optional.of(INDEXER)); + assertEquals(BASELINE, baselineIndexer.createIndexedBaseline(BASELINE)); + } + + @Test + void shouldReturnSameBaselineIfIndexerNameSetButItDoesntExist() + { + baselineIndexer = new BaselineIndexer(Map.of(), Optional.empty()); + assertEquals(BASELINE, baselineIndexer.createIndexedBaseline(BASELINE)); + } + + @Test + void shouldModifyBaselineName() + { + var indexer = mock(IScreenshotIndexer.class); + baselineIndexer = new BaselineIndexer(Map.of(INDEXER, indexer), Optional.of(INDEXER)); + var indexedBaseline = "baseline-1"; + when(indexer.index(BASELINE)).thenReturn(indexedBaseline); + assertEquals(indexedBaseline, baselineIndexer.createIndexedBaseline(BASELINE)); + } +} diff --git a/vividus-extension-visual-testing/src/test/java/org/vividus/visual/screenshot/IgnoreStrategyTests.java b/vividus-extension-visual-testing/src/test/java/org/vividus/visual/screenshot/IgnoreStrategyTests.java index 04bc3f2bd2..fd5801cb4b 100644 --- a/vividus-extension-visual-testing/src/test/java/org/vividus/visual/screenshot/IgnoreStrategyTests.java +++ b/vividus-extension-visual-testing/src/test/java/org/vividus/visual/screenshot/IgnoreStrategyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2020 the original author or authors. + * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import org.junit.jupiter.api.Test; import org.mockito.InOrder; import org.mockito.Mockito; +import org.vividus.selenium.screenshot.IgnoreStrategy; import ru.yandex.qatools.ashot.coordinates.Coords; diff --git a/vividus-extension-visual-testing/src/test/java/org/vividus/visual/steps/VisualStepsTests.java b/vividus-extension-visual-testing/src/test/java/org/vividus/visual/steps/VisualStepsTests.java index a43304c9bb..ed391b34b8 100644 --- a/vividus-extension-visual-testing/src/test/java/org/vividus/visual/steps/VisualStepsTests.java +++ b/vividus-extension-visual-testing/src/test/java/org/vividus/visual/steps/VisualStepsTests.java @@ -16,46 +16,66 @@ package org.vividus.visual.steps; +import static com.github.valfirst.slf4jtest.LoggingEvent.warn; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; +import com.github.valfirst.slf4jtest.LoggingEvent; +import com.github.valfirst.slf4jtest.TestLogger; +import com.github.valfirst.slf4jtest.TestLoggerFactory; +import com.github.valfirst.slf4jtest.TestLoggerFactoryExtension; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -import org.mockito.InOrder; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.openqa.selenium.SearchContext; import org.vividus.reporter.event.IAttachmentPublisher; +import org.vividus.selenium.screenshot.IgnoreStrategy; import org.vividus.softassert.ISoftAssert; +import org.vividus.ui.action.search.Locator; import org.vividus.ui.context.IUiContext; +import org.vividus.ui.screenshot.ScreenshotConfiguration; import org.vividus.ui.screenshot.ScreenshotPrecondtionMismatchException; import org.vividus.visual.model.AbstractVisualCheck; import org.vividus.visual.model.VisualActionType; import org.vividus.visual.model.VisualCheckResult; -@ExtendWith(MockitoExtension.class) +@ExtendWith({ MockitoExtension.class, TestLoggerFactoryExtension.class }) class VisualStepsTests { private static final String TEMPLATE = "template"; + private static final String SOURCE_KEY = "source-key"; + + private static final LoggingEvent WARNING_MESSAGE = warn("The passing of elements and areas to ignore through {}" + + " is deprecated, please use screenshot configuration instead", SOURCE_KEY); @Mock private IUiContext uiContext; @Mock private IAttachmentPublisher attachmentPublisher; @Mock private ISoftAssert softAssert; @InjectMocks private TestVisualSteps visualSteps; + private final TestLogger testLogger = TestLoggerFactory.getTestLogger(AbstractVisualSteps.class); + @SuppressWarnings("unchecked") @Test void shouldRecordAssertionWhenContextIsNull() @@ -63,19 +83,19 @@ void shouldRecordAssertionWhenContextIsNull() Function checkResultProvider = mock(Function.class); Supplier visualCheckFactory = mock(Supplier.class); when(uiContext.getOptionalSearchContext()).thenReturn(Optional.empty()); - visualSteps.execute(checkResultProvider, visualCheckFactory, TEMPLATE); + visualSteps.execute(visualCheckFactory, checkResultProvider, TEMPLATE); verifyNoInteractions(visualCheckFactory, checkResultProvider, attachmentPublisher); } @Test void shouldNotPublishAttachmentWhenResultIsNull() { - SearchContext searchContext = mock(SearchContext.class); - AbstractVisualCheck visualCheck = mock(AbstractVisualCheck.class); + var searchContext = mock(SearchContext.class); + var visualCheck = mock(AbstractVisualCheck.class); when(uiContext.getOptionalSearchContext()).thenReturn(Optional.of(searchContext)); - Function checkResultProvider = check -> null; - Supplier visualCheckFactory = () -> visualCheck; - visualSteps.execute(checkResultProvider, visualCheckFactory, TEMPLATE); + var checkResultProvider = (Function) check -> null; + var visualCheckFactory = (Supplier) () -> visualCheck; + visualSteps.execute(visualCheckFactory, checkResultProvider, TEMPLATE); verifyNoInteractions(attachmentPublisher); verify(visualCheck).setSearchContext(searchContext); } @@ -84,16 +104,16 @@ void shouldNotPublishAttachmentWhenResultIsNull() @CsvSource({"true, COMPARE_AGAINST", "false, CHECK_INEQUALITY_AGAINST"}) void shouldPublishAttachment(boolean passed, VisualActionType action) { - SearchContext searchContext = mock(SearchContext.class); - AbstractVisualCheck visualCheck = mock(AbstractVisualCheck.class); - VisualCheckResult visualCheckResult = mock(VisualCheckResult.class); + var searchContext = mock(SearchContext.class); + var visualCheck = mock(AbstractVisualCheck.class); + var visualCheckResult = mock(VisualCheckResult.class); when(visualCheckResult.getActionType()).thenReturn(action); when(uiContext.getOptionalSearchContext()).thenReturn(Optional.of(searchContext)); - Function checkResultProvider = check -> visualCheckResult; - Supplier visualCheckFactory = () -> visualCheck; + var checkResultProvider = (Function) check -> visualCheckResult; + var visualCheckFactory = (Supplier) () -> visualCheck; when(visualCheckResult.isPassed()).thenReturn(passed); - visualSteps.execute(checkResultProvider, visualCheckFactory, TEMPLATE); - InOrder ordered = Mockito.inOrder(attachmentPublisher, visualCheckResult, softAssert); + visualSteps.execute(visualCheckFactory, checkResultProvider, TEMPLATE); + var ordered = Mockito.inOrder(attachmentPublisher, visualCheckResult, softAssert); ordered.verify(attachmentPublisher).publishAttachment(TEMPLATE, Map.of("result", visualCheckResult), "Visual comparison"); ordered.verify(softAssert).assertTrue("Visual check passed", true); @@ -110,19 +130,66 @@ void shouldReturnsSoftAssert() @Test void shouldRecordInvalidVisualCheckPreconditionException() { - SearchContext searchContext = mock(SearchContext.class); - ScreenshotPrecondtionMismatchException exception = mock(ScreenshotPrecondtionMismatchException.class); + var searchContext = mock(SearchContext.class); + var exception = mock(ScreenshotPrecondtionMismatchException.class); Supplier visualCheckFactory = mock(Supplier.class); doThrow(exception).when(visualCheckFactory).get(); Function checkResultProvider = mock(Function.class); when(uiContext.getOptionalSearchContext()).thenReturn(Optional.of(searchContext)); - visualSteps.execute(checkResultProvider, visualCheckFactory, TEMPLATE); + visualSteps.execute(visualCheckFactory, checkResultProvider, TEMPLATE); verify(softAssert).recordFailedAssertion(exception); verifyNoInteractions(attachmentPublisher, checkResultProvider); } + @Test + void shouldFailIfBothSourcesAreNotEmpty() + { + var locator = mock(Locator.class); + var screenshotConfiguration = new ScreenshotConfiguration(); + screenshotConfiguration.setAreasToIgnore(Set.of(locator)); + screenshotConfiguration.setElementsToIgnore(Set.of(locator)); + Map> ignores = Map.of( + IgnoreStrategy.AREA, Set.of(locator), + IgnoreStrategy.ELEMENT, Set.of(locator) + ); + var thrown = assertThrows(IllegalArgumentException.class, + () -> visualSteps.patchIgnores(SOURCE_KEY, screenshotConfiguration, ignores)); + assertEquals("The elements and areas to ignore must be passed either through screenshot configuration or " + + SOURCE_KEY, thrown.getMessage()); + } + + @Test + void shouldNotPatchIgnores() + { + var locator = mock(Locator.class); + var screenshotConfiguration = new ScreenshotConfiguration(); + screenshotConfiguration.setAreasToIgnore(Set.of(locator)); + screenshotConfiguration.setElementsToIgnore(Set.of(locator)); + visualSteps.patchIgnores(SOURCE_KEY, screenshotConfiguration, Map.of( + IgnoreStrategy.AREA, Set.of(), + IgnoreStrategy.ELEMENT, Set.of() + )); + assertEquals(screenshotConfiguration.getElementsToIgnore(), Set.of(locator)); + assertEquals(screenshotConfiguration.getAreasToIgnore(), Set.of(locator)); + assertThat(testLogger.getLoggingEvents(), equalTo(List.of())); + } + + @Test + void shouldPatchIgnores() + { + var locator = mock(Locator.class); + var screenshotConfiguration = new ScreenshotConfiguration(); + visualSteps.patchIgnores(SOURCE_KEY, screenshotConfiguration, Map.of( + IgnoreStrategy.AREA, Set.of(locator), + IgnoreStrategy.ELEMENT, Set.of(locator) + )); + assertEquals(screenshotConfiguration.getElementsToIgnore(), Set.of(locator)); + assertEquals(screenshotConfiguration.getAreasToIgnore(), Set.of(locator)); + assertThat(testLogger.getLoggingEvents(), equalTo(List.of(WARNING_MESSAGE, WARNING_MESSAGE))); + } + private static final class TestVisualSteps extends AbstractVisualSteps { private TestVisualSteps(IUiContext uiContext, IAttachmentPublisher attachmentPublisher, ISoftAssert softAssert) diff --git a/vividus-plugin-applitools/build.gradle b/vividus-plugin-applitools/build.gradle index fada998c88..595b2f92fd 100644 --- a/vividus-plugin-applitools/build.gradle +++ b/vividus-plugin-applitools/build.gradle @@ -9,7 +9,6 @@ dependencies { implementation project(':vividus-soft-assert') implementation project(':vividus-util') - implementation(group: 'javax.inject', name: 'javax.inject', version: versions.javaxInject) implementation(group: 'com.applitools', name: 'eyes-images-java3', version: '3.213.0') implementation(group: 'org.slf4j', name: 'slf4j-api', version: versions.slf4j) diff --git a/vividus-plugin-applitools/src/main/java/org/vividus/converter/ExamplesTableToApplitoolsVisualChecksConverter.java b/vividus-plugin-applitools/src/main/java/org/vividus/converter/ExamplesTableToApplitoolsVisualChecksConverter.java index 78bc7ae9b8..3a6c64d812 100644 --- a/vividus-plugin-applitools/src/main/java/org/vividus/converter/ExamplesTableToApplitoolsVisualChecksConverter.java +++ b/vividus-plugin-applitools/src/main/java/org/vividus/converter/ExamplesTableToApplitoolsVisualChecksConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 the original author or authors. + * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,7 +47,6 @@ public List convertValue(ExamplesTable toConvert, Type ty checkNotNull(o.getAction(), "action"); }) .map(visualCheckFactory::unite) - .peek(ApplitoolsVisualCheck::buildIgnores) .collect(Collectors.toList()); } diff --git a/vividus-plugin-applitools/src/main/java/org/vividus/visual/eyes/VisualTestingSteps.java b/vividus-plugin-applitools/src/main/java/org/vividus/visual/eyes/VisualTestingSteps.java index eb41344ee3..d8f611381b 100644 --- a/vividus-plugin-applitools/src/main/java/org/vividus/visual/eyes/VisualTestingSteps.java +++ b/vividus-plugin-applitools/src/main/java/org/vividus/visual/eyes/VisualTestingSteps.java @@ -17,12 +17,16 @@ package org.vividus.visual.eyes; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.Supplier; import org.jbehave.core.annotations.When; import org.vividus.reporter.event.IAttachmentPublisher; +import org.vividus.selenium.screenshot.IgnoreStrategy; import org.vividus.softassert.ISoftAssert; +import org.vividus.ui.action.search.Locator; import org.vividus.ui.context.IUiContext; import org.vividus.ui.screenshot.ScreenshotConfiguration; import org.vividus.ui.screenshot.ScreenshotParameters; @@ -183,11 +187,27 @@ public void performCheck(List applitoolsConfigurations, .forEach(visualCheck -> runApplitoolsTest(visualCheck, Optional.of(screenshotConfiguration))); } - private void runApplitoolsTest(ApplitoolsVisualCheck visualCheck, Optional configuration) + private void runApplitoolsTest(ApplitoolsVisualCheck visualCheck, + Optional screenshotConfiguration) { runApplitoolsTest(() -> { - Optional screenshotParameters = screenshotParametersFactory.create(configuration); + Map> ignores = Map.of( + IgnoreStrategy.AREA, visualCheck.getAreasToIgnore(), + IgnoreStrategy.ELEMENT, visualCheck.getElementsToIgnore() + ); + + Optional screenshotParameters; + if (screenshotConfiguration.isPresent()) + { + patchIgnores("applitools configuration", screenshotConfiguration.get(), ignores); + screenshotParameters = screenshotParametersFactory.create(screenshotConfiguration); + } + else + { + screenshotParameters = screenshotParametersFactory.create(ignores); + } + visualCheck.setScreenshotParameters(screenshotParameters); return visualCheck; }); @@ -195,6 +215,6 @@ private void runApplitoolsTest(ApplitoolsVisualCheck visualCheck, Optional applitoolsVisualCheckSupplier) { - execute(visualTestingService::run, applitoolsVisualCheckSupplier, "applitools-visual-comparison.ftl"); + execute(applitoolsVisualCheckSupplier, visualTestingService::run, "applitools-visual-comparison.ftl"); } } diff --git a/vividus-plugin-applitools/src/main/java/org/vividus/visual/eyes/factory/ApplitoolsVisualCheckFactory.java b/vividus-plugin-applitools/src/main/java/org/vividus/visual/eyes/factory/ApplitoolsVisualCheckFactory.java index 2a39c5130f..d6a231c840 100644 --- a/vividus-plugin-applitools/src/main/java/org/vividus/visual/eyes/factory/ApplitoolsVisualCheckFactory.java +++ b/vividus-plugin-applitools/src/main/java/org/vividus/visual/eyes/factory/ApplitoolsVisualCheckFactory.java @@ -23,13 +23,18 @@ import com.applitools.eyes.MatchLevel; +import org.vividus.ui.screenshot.ScreenshotConfiguration; +import org.vividus.ui.screenshot.ScreenshotParameters; import org.vividus.ui.screenshot.ScreenshotParametersFactory; -import org.vividus.visual.AbstractVisualCheckFactory; import org.vividus.visual.eyes.model.ApplitoolsVisualCheck; import org.vividus.visual.model.VisualActionType; +import org.vividus.visual.screenshot.BaselineIndexer; -public class ApplitoolsVisualCheckFactory extends AbstractVisualCheckFactory +public class ApplitoolsVisualCheckFactory { + private final ScreenshotParametersFactory screenshotParametersFactory; + private final BaselineIndexer baselineIndexer; + private String executeApiKey; private String readApiKey; private String hostApp; @@ -40,17 +45,21 @@ public class ApplitoolsVisualCheckFactory extends AbstractVisualCheckFactory screenshotParameters = screenshotParametersFactory.create(Optional.empty()); + + ApplitoolsVisualCheck check = new ApplitoolsVisualCheck(batchName, + baselineIndexer.createIndexedBaseline(baselineName), action); + check.setScreenshotParameters(screenshotParameters); check.setExecuteApiKey(executeApiKey); check.setReadApiKey(readApiKey); check.setBaselineEnvName(baselineEnvName); diff --git a/vividus-plugin-applitools/src/main/java/org/vividus/visual/eyes/factory/ImageEyesFactory.java b/vividus-plugin-applitools/src/main/java/org/vividus/visual/eyes/factory/ImageEyesFactory.java index e8d51f2943..3c6212e506 100644 --- a/vividus-plugin-applitools/src/main/java/org/vividus/visual/eyes/factory/ImageEyesFactory.java +++ b/vividus-plugin-applitools/src/main/java/org/vividus/visual/eyes/factory/ImageEyesFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2020 the original author or authors. + * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,8 +19,6 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import javax.inject.Named; - import com.applitools.eyes.BatchInfo; import com.applitools.eyes.LogHandler; import com.applitools.eyes.RectangleSize; @@ -29,7 +27,6 @@ import org.vividus.visual.eyes.model.ApplitoolsVisualCheck; import org.vividus.visual.model.VisualActionType; -@Named public class ImageEyesFactory { private final LogHandler logHandler; diff --git a/vividus-plugin-applitools/src/main/java/org/vividus/visual/eyes/model/ApplitoolsVisualCheck.java b/vividus-plugin-applitools/src/main/java/org/vividus/visual/eyes/model/ApplitoolsVisualCheck.java index fde6985acc..0e7d8f3167 100644 --- a/vividus-plugin-applitools/src/main/java/org/vividus/visual/eyes/model/ApplitoolsVisualCheck.java +++ b/vividus-plugin-applitools/src/main/java/org/vividus/visual/eyes/model/ApplitoolsVisualCheck.java @@ -17,7 +17,6 @@ package org.vividus.visual.eyes.model; import java.net.URI; -import java.util.Map; import java.util.Set; import com.applitools.eyes.MatchLevel; @@ -25,7 +24,6 @@ import org.vividus.ui.action.search.Locator; import org.vividus.visual.model.AbstractVisualCheck; import org.vividus.visual.model.VisualActionType; -import org.vividus.visual.screenshot.IgnoreStrategy; public class ApplitoolsVisualCheck extends AbstractVisualCheck { @@ -53,11 +51,6 @@ public ApplitoolsVisualCheck(String batchName, String baselineName, VisualAction this.batchName = batchName; } - public void buildIgnores() - { - setElementsToIgnore(Map.of(IgnoreStrategy.AREA, areasToIgnore, IgnoreStrategy.ELEMENT, elementsToIgnore)); - } - public String getExecuteApiKey() { return executeApiKey; @@ -162,4 +155,14 @@ public void setAreasToIgnore(Set areasToIgnore) { this.areasToIgnore = areasToIgnore; } + + public Set getElementsToIgnore() + { + return elementsToIgnore; + } + + public Set getAreasToIgnore() + { + return areasToIgnore; + } } diff --git a/vividus-plugin-applitools/src/main/java/org/vividus/visual/eyes/service/ImageVisualTestingService.java b/vividus-plugin-applitools/src/main/java/org/vividus/visual/eyes/service/ImageVisualTestingService.java index af59bb402b..fe19b55cc1 100644 --- a/vividus-plugin-applitools/src/main/java/org/vividus/visual/eyes/service/ImageVisualTestingService.java +++ b/vividus-plugin-applitools/src/main/java/org/vividus/visual/eyes/service/ImageVisualTestingService.java @@ -20,8 +20,6 @@ import java.net.URI; import java.util.Base64; -import javax.inject.Named; - import com.applitools.eyes.StepInfo; import com.applitools.eyes.StepInfo.ApiUrls; import com.applitools.eyes.TestResults; @@ -33,25 +31,27 @@ import org.slf4j.LoggerFactory; import org.vividus.http.client.HttpResponse; import org.vividus.http.client.IHttpClient; +import org.vividus.selenium.screenshot.AshotScreenshotTaker; +import org.vividus.ui.screenshot.ScreenshotParameters; import org.vividus.visual.eyes.factory.ImageEyesFactory; import org.vividus.visual.eyes.model.ApplitoolsVisualCheck; import org.vividus.visual.eyes.model.ApplitoolsVisualCheckResult; -import org.vividus.visual.screenshot.ScreenshotProvider; -@Named +import ru.yandex.qatools.ashot.Screenshot; + public class ImageVisualTestingService implements VisualTestingService { private static final Logger LOGGER = LoggerFactory.getLogger(ImageVisualTestingService.class); private final ImageEyesFactory eyesFactory; - private final ScreenshotProvider screenshotProvider; + private final AshotScreenshotTaker ashotScreenshotTaker; private final IHttpClient httpClient; - public ImageVisualTestingService(ImageEyesFactory eyesFactory, ScreenshotProvider screenshotProvider, - @Named("eyesHttpClient") IHttpClient httpClient) + public ImageVisualTestingService(ImageEyesFactory eyesFactory, + AshotScreenshotTaker ashotScreenshotTaker, IHttpClient httpClient) { this.eyesFactory = eyesFactory; - this.screenshotProvider = screenshotProvider; + this.ashotScreenshotTaker = ashotScreenshotTaker; this.httpClient = httpClient; } @@ -63,7 +63,9 @@ public ApplitoolsVisualCheckResult run(ApplitoolsVisualCheck applitoolsVisualChe try { eyes.open(applitoolsVisualCheck.getAppName(), applitoolsVisualCheck.getBaselineName()); - eyes.checkImage(screenshotProvider.take(applitoolsVisualCheck).getImage()); + Screenshot screenshot = ashotScreenshotTaker.takeAshotScreenshot(applitoolsVisualCheck.getSearchContext(), + applitoolsVisualCheck.getScreenshotParameters()); + eyes.checkImage(screenshot.getImage()); } finally { diff --git a/vividus-plugin-applitools/src/main/resources/spring.xml b/vividus-plugin-applitools/src/main/resources/spring.xml index 048d49cc97..59450404c8 100644 --- a/vividus-plugin-applitools/src/main/resources/spring.xml +++ b/vividus-plugin-applitools/src/main/resources/spring.xml @@ -1,25 +1,20 @@ - - - - + + @@ -53,6 +48,14 @@ + + + + + + + + diff --git a/vividus-plugin-applitools/src/test/java/org/vividus/converter/ExamplesTableToApplitoolsVisualChecksConverterTests.java b/vividus-plugin-applitools/src/test/java/org/vividus/converter/ExamplesTableToApplitoolsVisualChecksConverterTests.java index aa52d79b6d..b1d9568b74 100644 --- a/vividus-plugin-applitools/src/test/java/org/vividus/converter/ExamplesTableToApplitoolsVisualChecksConverterTests.java +++ b/vividus-plugin-applitools/src/test/java/org/vividus/converter/ExamplesTableToApplitoolsVisualChecksConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 the original author or authors. + * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -62,7 +62,6 @@ void shouldConvertExamplesTableIntoApplitoolsVisualChecks() when(applitoolsVisualCheckFactory.unite(visualCheck)).thenReturn(visualCheck); assertEquals(checks, converter.convertValue(examplesTable, null)); verify(applitoolsVisualCheckFactory, times(2)).unite(visualCheck); - verify(visualCheck, times(2)).buildIgnores(); } private ExamplesTable mockExamplesTable(List checks) diff --git a/vividus-plugin-applitools/src/test/java/org/vividus/visual/eyes/VisualTestingStepsTests.java b/vividus-plugin-applitools/src/test/java/org/vividus/visual/eyes/VisualTestingStepsTests.java index 83fa72cbed..9f3632d2e0 100644 --- a/vividus-plugin-applitools/src/test/java/org/vividus/visual/eyes/VisualTestingStepsTests.java +++ b/vividus-plugin-applitools/src/test/java/org/vividus/visual/eyes/VisualTestingStepsTests.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -32,7 +33,9 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.openqa.selenium.SearchContext; import org.vividus.reporter.event.IAttachmentPublisher; +import org.vividus.selenium.screenshot.IgnoreStrategy; import org.vividus.softassert.ISoftAssert; +import org.vividus.ui.action.search.Locator; import org.vividus.ui.context.IUiContext; import org.vividus.ui.screenshot.ScreenshotConfiguration; import org.vividus.ui.screenshot.ScreenshotParametersFactory; @@ -103,4 +106,22 @@ void shouldRunApplitoolsVisualCheckWithCustomConfiguration() verifyVisualCheck(result, 2); verify(check, times(2)).setScreenshotParameters(Optional.of(screenshotParameters)); } + + @Test + void shouldRunApplitoolsVisualCheckUsingApplitoolsConfiguration() + { + ApplitoolsVisualCheck check = mock(ApplitoolsVisualCheck.class); + ApplitoolsVisualCheckResult result = mock(ApplitoolsVisualCheckResult.class); + when(visualTestingService.run(check)).thenReturn(result); + WebScreenshotParameters screenshotParameters = mock(WebScreenshotParameters.class); + Locator locator = mock(Locator.class); + when(check.getElementsToIgnore()).thenReturn(Set.of(locator)); + when(check.getAreasToIgnore()).thenReturn(Set.of(locator)); + when(screenshotParametersFactory + .create(Map.of(IgnoreStrategy.AREA, Set.of(locator), IgnoreStrategy.ELEMENT, Set.of(locator)))) + .thenReturn(Optional.of(screenshotParameters)); + visualTestingSteps.performCheck(List.of(check)); + verifyVisualCheck(result, 1); + verify(check).setScreenshotParameters(Optional.of(screenshotParameters)); + } } diff --git a/vividus-plugin-applitools/src/test/java/org/vividus/visual/eyes/factory/ApplitoolsVisualCheckFactoryTests.java b/vividus-plugin-applitools/src/test/java/org/vividus/visual/eyes/factory/ApplitoolsVisualCheckFactoryTests.java index 954d5b8131..b833f1c0c6 100644 --- a/vividus-plugin-applitools/src/test/java/org/vividus/visual/eyes/factory/ApplitoolsVisualCheckFactoryTests.java +++ b/vividus-plugin-applitools/src/test/java/org/vividus/visual/eyes/factory/ApplitoolsVisualCheckFactoryTests.java @@ -23,7 +23,6 @@ import static org.mockito.Mockito.when; import java.net.URI; -import java.util.Map; import java.util.Optional; import java.util.function.BiFunction; @@ -41,6 +40,7 @@ import org.vividus.ui.screenshot.ScreenshotParametersFactory; import org.vividus.visual.eyes.model.ApplitoolsVisualCheck; import org.vividus.visual.model.VisualActionType; +import org.vividus.visual.screenshot.BaselineIndexer; @ExtendWith(MockitoExtension.class) class ApplitoolsVisualCheckFactoryTests @@ -67,11 +67,9 @@ class ApplitoolsVisualCheckFactoryTests private static final String BASELINE_ENV_NAME = "baselineEnvName"; - @Mock - private ScreenshotParametersFactory screenshotParametersFactory; - - @InjectMocks - private ApplitoolsVisualCheckFactory factory; + @Mock private ScreenshotParametersFactory screenshotParametersFactory; + @Mock private BaselineIndexer baselineIndexer; + @InjectMocks private ApplitoolsVisualCheckFactory factory; @BeforeEach void setUp() @@ -84,13 +82,12 @@ void setUp() factory.setReadApiKey(READ_API_KEY); factory.setServerUri(SERVER_URI); factory.setViewportSize(VIEWPORT_SIZE); - factory.setScreenshotIndexer(Optional.empty()); - factory.setIndexers(Map.of()); } @Test void shouldCreateApplitoolsVisualCheckAndSetDefaultProperties() { + when(baselineIndexer.createIndexedBaseline(BASELINE)).thenReturn(BASELINE); var screenshotParameters = mock(ScreenshotParameters.class); when(screenshotParametersFactory.create(Optional.empty())).thenReturn(Optional.of(screenshotParameters)); var applitoolsVisualCheck = factory.create(BATCH_NAME, BASELINE, ACTION); diff --git a/vividus-plugin-applitools/src/test/java/org/vividus/visual/eyes/model/ApplitoolsVisualCheckTests.java b/vividus-plugin-applitools/src/test/java/org/vividus/visual/eyes/model/ApplitoolsVisualCheckTests.java index edff0b51ee..353ea05318 100644 --- a/vividus-plugin-applitools/src/test/java/org/vividus/visual/eyes/model/ApplitoolsVisualCheckTests.java +++ b/vividus-plugin-applitools/src/test/java/org/vividus/visual/eyes/model/ApplitoolsVisualCheckTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2020 the original author or authors. + * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,13 +18,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import java.util.Map; import java.util.Set; import org.junit.jupiter.api.Test; import org.vividus.ui.action.search.Locator; import org.vividus.ui.web.action.search.WebLocatorType; -import org.vividus.visual.screenshot.IgnoreStrategy; class ApplitoolsVisualCheckTests { @@ -33,20 +31,18 @@ class ApplitoolsVisualCheckTests @Test void shouldUseEmptySetsAsDefaultIgnores() { - visualCheck.buildIgnores(); - assertEquals(Map.of(IgnoreStrategy.AREA, Set.of(), IgnoreStrategy.ELEMENT, Set.of()), - visualCheck.getElementsToIgnore()); + assertEquals(Set.of(), visualCheck.getElementsToIgnore()); + assertEquals(Set.of(), visualCheck.getAreasToIgnore()); } @Test void shouldFillElementsToIgnoreWithValues() { - Set element = Set.of(new Locator(WebLocatorType.ID, "element")); - Set area = Set.of(new Locator(WebLocatorType.ID, "area")); - visualCheck.setElementsToIgnore(element); - visualCheck.setAreasToIgnore(area); - visualCheck.buildIgnores(); - assertEquals(Map.of(IgnoreStrategy.AREA, area, IgnoreStrategy.ELEMENT, element), - visualCheck.getElementsToIgnore()); + Set elements = Set.of(new Locator(WebLocatorType.ID, "element")); + Set areas = Set.of(new Locator(WebLocatorType.ID, "area")); + visualCheck.setElementsToIgnore(elements); + visualCheck.setAreasToIgnore(areas); + assertEquals(elements, visualCheck.getElementsToIgnore()); + assertEquals(areas, visualCheck.getAreasToIgnore()); } } diff --git a/vividus-plugin-applitools/src/test/java/org/vividus/visual/eyes/service/ImageVisualTestingServiceTests.java b/vividus-plugin-applitools/src/test/java/org/vividus/visual/eyes/service/ImageVisualTestingServiceTests.java index 5589b0121f..d8beb9424b 100644 --- a/vividus-plugin-applitools/src/test/java/org/vividus/visual/eyes/service/ImageVisualTestingServiceTests.java +++ b/vividus-plugin-applitools/src/test/java/org/vividus/visual/eyes/service/ImageVisualTestingServiceTests.java @@ -53,11 +53,12 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.vividus.http.client.HttpResponse; import org.vividus.http.client.IHttpClient; +import org.vividus.selenium.screenshot.AshotScreenshotTaker; +import org.vividus.ui.screenshot.ScreenshotParameters; import org.vividus.visual.eyes.factory.ImageEyesFactory; import org.vividus.visual.eyes.model.ApplitoolsVisualCheck; import org.vividus.visual.eyes.model.ApplitoolsVisualCheckResult; import org.vividus.visual.model.VisualActionType; -import org.vividus.visual.screenshot.ScreenshotProvider; import ru.yandex.qatools.ashot.Screenshot; @@ -85,11 +86,12 @@ class ImageVisualTestingServiceTests private static final String BATCH_NAME = "batchName"; private final ImageEyesFactory eyesFactory = mock(ImageEyesFactory.class); - private final ScreenshotProvider screenshotProvider = mock(ScreenshotProvider.class); + @SuppressWarnings("unchecked") + private final AshotScreenshotTaker ashotScreenshotTaker = mock(AshotScreenshotTaker.class); private final IHttpClient httpClient = mock(IHttpClient.class); @InjectMocks private final ImageVisualTestingService imageVisualTestingService - = new ImageVisualTestingService(eyesFactory, screenshotProvider, httpClient); + = new ImageVisualTestingService(eyesFactory, ashotScreenshotTaker, httpClient); @Test void shouldRunVisualTestAndPublishResults() throws IOException @@ -204,7 +206,8 @@ void shouldLogWarningWhenUrlUnauthorized() throws IOException private BufferedImage mockScreenshot(ApplitoolsVisualCheck applitoolsVisualCheck) { Screenshot screenshot = mock(Screenshot.class); - when(screenshotProvider.take(applitoolsVisualCheck)).thenReturn(screenshot); + when(ashotScreenshotTaker.takeAshotScreenshot(applitoolsVisualCheck.getSearchContext(), + applitoolsVisualCheck.getScreenshotParameters())).thenReturn(screenshot); BufferedImage image = mock(BufferedImage.class); when(screenshot.getImage()).thenReturn(image); return image; diff --git a/vividus-plugin-mobile-app/src/main/java/org/vividus/selenium/mobileapp/screenshot/MobileAppAshotFactory.java b/vividus-plugin-mobile-app/src/main/java/org/vividus/selenium/mobileapp/screenshot/MobileAppAshotFactory.java index 031ee8c774..16cca32fab 100644 --- a/vividus-plugin-mobile-app/src/main/java/org/vividus/selenium/mobileapp/screenshot/MobileAppAshotFactory.java +++ b/vividus-plugin-mobile-app/src/main/java/org/vividus/selenium/mobileapp/screenshot/MobileAppAshotFactory.java @@ -24,6 +24,7 @@ import org.vividus.selenium.mobileapp.MobileAppWebDriverManager; import org.vividus.selenium.mobileapp.screenshot.util.CoordsUtils; import org.vividus.selenium.screenshot.AbstractAshotFactory; +import org.vividus.selenium.screenshot.ScreenshotCropper; import org.vividus.ui.screenshot.ScreenshotParameters; import ru.yandex.qatools.ashot.AShot; @@ -36,8 +37,10 @@ public class MobileAppAshotFactory extends AbstractAshotFactory screenshotParameters) } int nativeFooterToCut = screenshotParameters.map(ScreenshotParameters::getNativeFooterToCut).orElse(0); strategy = decorateWithFixedCutStrategy(strategy, statusBarSize, nativeFooterToCut); + + strategy = decorateWithCropping(strategy, screenshotParameters); + return new AShot().shootingStrategy(strategy).coordsProvider(coordsProvider); } diff --git a/vividus-plugin-mobile-app/src/main/java/org/vividus/selenium/mobileapp/screenshot/MobileAppCoordsProvider.java b/vividus-plugin-mobile-app/src/main/java/org/vividus/selenium/mobileapp/screenshot/MobileAppCoordsProvider.java index 3a8af04302..13e686b44a 100644 --- a/vividus-plugin-mobile-app/src/main/java/org/vividus/selenium/mobileapp/screenshot/MobileAppCoordsProvider.java +++ b/vividus-plugin-mobile-app/src/main/java/org/vividus/selenium/mobileapp/screenshot/MobileAppCoordsProvider.java @@ -20,38 +20,58 @@ import org.openqa.selenium.WebElement; import org.vividus.selenium.mobileapp.MobileAppWebDriverManager; import org.vividus.selenium.mobileapp.screenshot.util.CoordsUtils; -import org.vividus.selenium.screenshot.AbstractAdjustingCoordsProvider; import org.vividus.ui.context.IUiContext; import ru.yandex.qatools.ashot.coordinates.Coords; +import ru.yandex.qatools.ashot.coordinates.WebDriverCoordsProvider; -public class MobileAppCoordsProvider extends AbstractAdjustingCoordsProvider +public class MobileAppCoordsProvider extends WebDriverCoordsProvider { private static final long serialVersionUID = 2966521618709606533L; private final transient MobileAppWebDriverManager mobileAppWebDriverManager; + private final transient IUiContext uiContext; private final boolean downscale; public MobileAppCoordsProvider(boolean downscale, MobileAppWebDriverManager mobileAppWebDriverManager, IUiContext uiContext) { - super(uiContext); this.mobileAppWebDriverManager = mobileAppWebDriverManager; + this.uiContext = uiContext; this.downscale = downscale; } @Override public Coords ofElement(WebDriver driver, WebElement element) { - Coords coords = getCoords(element); - coords = element.equals(getUiContext().getSearchContext()) ? coords : adjustToSearchContext(coords); - return downscale ? coords : adjustToDpr(coords); + Coords coords = super.ofElement(null, element); + Coords barSizeAdjustedCoords = cutBarSize(coords); + + if (downscale) + { + return barSizeAdjustedCoords; + } + + return uiContext.getOptionalSearchContext() + .filter(context -> !context.equals(element) && context instanceof WebElement) + .map(WebElement.class::cast) + .map(context -> super.ofElement(null, context)) + .map(contextCoords -> + { + Coords adjustedContext = adjustToDpr(cutBarSize(contextCoords)); + + coords.width = adjustToDpr(coords.width); + coords.height = adjustToDpr(coords.height); + coords.x = adjustedContext.x + adjustToDpr(coords.x - contextCoords.x); + coords.y = adjustedContext.y + adjustToDpr(coords.y - contextCoords.y); + + return coords; + }) + .orElseGet(() -> adjustToDpr(barSizeAdjustedCoords)); } - @Override - protected Coords getCoords(WebElement element) + private Coords cutBarSize(Coords coords) { - Coords coords = super.ofElement(null, element); return new Coords(coords.x, coords.y - mobileAppWebDriverManager.getStatusBarSize(), coords.width, coords.height); } @@ -61,4 +81,10 @@ private Coords adjustToDpr(Coords coords) double dpr = mobileAppWebDriverManager.getDpr(); return CoordsUtils.scale(coords, dpr); } + + private int adjustToDpr(int value) + { + double dpr = mobileAppWebDriverManager.getDpr(); + return CoordsUtils.scale(value, dpr); + } } diff --git a/vividus-plugin-mobile-app/src/main/java/org/vividus/ui/mobileapp/screenshot/MobileAppScreenshotParametersFactory.java b/vividus-plugin-mobile-app/src/main/java/org/vividus/ui/mobileapp/screenshot/MobileAppScreenshotParametersFactory.java index b4a26ab3bb..b4eddd5075 100644 --- a/vividus-plugin-mobile-app/src/main/java/org/vividus/ui/mobileapp/screenshot/MobileAppScreenshotParametersFactory.java +++ b/vividus-plugin-mobile-app/src/main/java/org/vividus/ui/mobileapp/screenshot/MobileAppScreenshotParametersFactory.java @@ -16,19 +16,43 @@ package org.vividus.ui.mobileapp.screenshot; +import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.function.BinaryOperator; +import org.vividus.selenium.screenshot.IgnoreStrategy; +import org.vividus.ui.action.search.Locator; import org.vividus.ui.screenshot.AbstractScreenshotParametersFactory; import org.vividus.ui.screenshot.ScreenshotConfiguration; import org.vividus.ui.screenshot.ScreenshotParameters; public class MobileAppScreenshotParametersFactory - extends AbstractScreenshotParametersFactory + extends AbstractScreenshotParametersFactory { @Override public Optional create(Optional screenshotConfiguration) { - return getScreenshotConfiguration(screenshotConfiguration, (p, b) -> + return getScreenshotConfiguration(screenshotConfiguration, getConfigurationMerger()) + .map(this::createWithBaseConfiguration); + } + + @Override + public Optional create(Map> ignores) + { + ScreenshotConfiguration configuration = getDefaultConfiguration().orElseGet(ScreenshotConfiguration::new); + return Optional.of(createWithBaseConfiguration(configuration, ignores)); + } + + @Override + protected ScreenshotParameters createScreenshotParameters() + { + return new ScreenshotParameters(); + } + + private BinaryOperator getConfigurationMerger() + { + return (p, b) -> { if (p.getNativeFooterToCut() == 0) { @@ -39,6 +63,6 @@ public Optional create(Optional s p.setShootingStrategy(b.getShootingStrategy()); } return p; - }).map(config -> createWithBaseConfiguration(config, ScreenshotParameters::new)); + }; } } diff --git a/vividus-plugin-mobile-app/src/main/resources/spring.xml b/vividus-plugin-mobile-app/src/main/resources/spring.xml index 06d32ff012..d0983d6169 100644 --- a/vividus-plugin-mobile-app/src/main/resources/spring.xml +++ b/vividus-plugin-mobile-app/src/main/resources/spring.xml @@ -35,7 +35,7 @@ - + @@ -104,7 +104,10 @@ - + + + diff --git a/vividus-plugin-mobile-app/src/test/java/org/vividus/selenium/mobileapp/screenshot/MobileAppAshotFactoryTests.java b/vividus-plugin-mobile-app/src/test/java/org/vividus/selenium/mobileapp/screenshot/MobileAppAshotFactoryTests.java index b2d5f13d97..e057a95a6d 100644 --- a/vividus-plugin-mobile-app/src/test/java/org/vividus/selenium/mobileapp/screenshot/MobileAppAshotFactoryTests.java +++ b/vividus-plugin-mobile-app/src/test/java/org/vividus/selenium/mobileapp/screenshot/MobileAppAshotFactoryTests.java @@ -39,6 +39,7 @@ import ru.yandex.qatools.ashot.AShot; import ru.yandex.qatools.ashot.coordinates.CoordsProvider; import ru.yandex.qatools.ashot.shooting.CuttingDecorator; +import ru.yandex.qatools.ashot.shooting.ElementCroppingDecorator; import ru.yandex.qatools.ashot.shooting.ShootingStrategy; import ru.yandex.qatools.ashot.shooting.cutter.CutStrategy; @@ -50,9 +51,12 @@ class MobileAppAshotFactoryTests private static final String DIMPLE = "dimple"; private static final String SIMPLE = "SIMPLE"; - @Mock private MobileAppWebDriverManager mobileAppWebDriverManager; - @Mock private CoordsProvider coordsProvider; - @InjectMocks private MobileAppAshotFactory ashotFactory; + @Mock + private MobileAppWebDriverManager mobileAppWebDriverManager; + @Mock + private CoordsProvider coordsProvider; + @InjectMocks + private MobileAppAshotFactory ashotFactory; @Test void shouldProvideDpr() @@ -63,16 +67,16 @@ void shouldProvideDpr() @SuppressWarnings("unchecked") @ParameterizedTest - @CsvSource({ - "true, 1, ru.yandex.qatools.ashot.shooting.ScalingDecorator", - "false, 2, ru.yandex.qatools.ashot.shooting.SimpleShootingStrategy" - }) + @CsvSource({ "true, 1, ru.yandex.qatools.ashot.shooting.ScalingDecorator", + "false, 2, ru.yandex.qatools.ashot.shooting.SimpleShootingStrategy" }) void shouldCreateAshotWithTheMergedConfiguration(boolean downscale, int headerToCut, Class strategyType) throws IllegalAccessException { mockAshotConfiguration(DIMPLE, downscale); AShot aShot = ashotFactory.create(createConfigurationWith(10, Optional.of(SIMPLE))); - CuttingDecorator strategy = (CuttingDecorator) FieldUtils.readField(aShot, SHOOTING_STRATEGY, true); + ElementCroppingDecorator croppingDecorator = (ElementCroppingDecorator) FieldUtils.readField(aShot, + SHOOTING_STRATEGY, true); + CuttingDecorator strategy = (CuttingDecorator) FieldUtils.readField(croppingDecorator, SHOOTING_STRATEGY, true); ShootingStrategy baseStrategy = (ShootingStrategy) FieldUtils.readField(strategy, SHOOTING_STRATEGY, true); CutStrategy cutStrategy = (CutStrategy) FieldUtils.readField(strategy, CUT_STRATEGY, true); assertEquals(strategyType, baseStrategy.getClass()); @@ -85,7 +89,8 @@ void shouldCreateAshotWithTheMergedConfiguration(boolean downscale, int headerTo private void mockAshotConfiguration(String defaultStrategy, boolean downscale) { ashotFactory.setDownscale(downscale); - ashotFactory.setStrategies(Map.of(SIMPLE, new SimpleScreenshotShootingStrategy(), DIMPLE, s -> { + ashotFactory.setStrategies(Map.of(SIMPLE, new SimpleScreenshotShootingStrategy(), DIMPLE, s -> + { throw new IllegalStateException(); })); when(mobileAppWebDriverManager.getDpr()).thenReturn(2d); diff --git a/vividus-plugin-mobile-app/src/test/java/org/vividus/selenium/mobileapp/screenshot/MobileAppCoordsProviderTests.java b/vividus-plugin-mobile-app/src/test/java/org/vividus/selenium/mobileapp/screenshot/MobileAppCoordsProviderTests.java index 2500b545bc..3a4abcb44e 100644 --- a/vividus-plugin-mobile-app/src/test/java/org/vividus/selenium/mobileapp/screenshot/MobileAppCoordsProviderTests.java +++ b/vividus-plugin-mobile-app/src/test/java/org/vividus/selenium/mobileapp/screenshot/MobileAppCoordsProviderTests.java @@ -17,11 +17,11 @@ package org.vividus.selenium.mobileapp.screenshot; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.util.Optional; import java.util.stream.Stream; import org.junit.jupiter.api.Assertions; @@ -31,14 +31,13 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; -import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.openqa.selenium.Dimension; import org.openqa.selenium.Point; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.vividus.selenium.mobileapp.MobileAppWebDriverManager; -import org.vividus.ui.context.UiContext; +import org.vividus.ui.context.IUiContext; import ru.yandex.qatools.ashot.coordinates.Coords; @@ -49,8 +48,8 @@ class MobileAppCoordsProviderTests private static final Dimension DIMENSION = new Dimension(1, 1); @Mock private MobileAppWebDriverManager driverManager; + @Mock private IUiContext uiContext; @Mock private WebElement webElement; - @Spy private UiContext uiContext; @Test void shouldProvideAdjustedWithNativeHeaderHeightCoordinates() @@ -59,7 +58,6 @@ void shouldProvideAdjustedWithNativeHeaderHeightCoordinates() when(driverManager.getStatusBarSize()).thenReturn(100); when(webElement.getLocation()).thenReturn(new Point(0, 234)); when(webElement.getSize()).thenReturn(DIMENSION); - doReturn(null).when(uiContext).getSearchContext(); Coords coords = coordsProvider.ofElement(null, webElement); Assertions.assertAll(() -> assertEquals(0, coords.getX()), () -> assertEquals(134, coords.getY()), @@ -67,23 +65,6 @@ void shouldProvideAdjustedWithNativeHeaderHeightCoordinates() () -> assertEquals(1, coords.getHeight())); } - @Test - void shouldAdjustElementCoordsToTheCurrentSearchContext() - { - MobileAppCoordsProvider coordsProvider = new MobileAppCoordsProvider(true, driverManager, uiContext); - WebElement contextElement = mock(WebElement.class); - when(contextElement.getLocation()).thenReturn(POINT); - when(contextElement.getSize()).thenReturn(new Dimension(100, 50)); - when(webElement.getLocation()).thenReturn(new Point(5, 15)); - when(webElement.getSize()).thenReturn(new Dimension(150, 30)); - doReturn(contextElement).when(uiContext).getSearchContext(); - Coords coords = coordsProvider.ofElement(null, webElement); - Assertions.assertAll(() -> assertEquals(0, coords.getX()), - () -> assertEquals(5, coords.getY()), - () -> assertEquals(100, coords.getWidth()), - () -> assertEquals(30, coords.getHeight())); - } - @Test void shouldNotAdjustCoordsForTheCurrentSearchContext() { @@ -91,7 +72,6 @@ void shouldNotAdjustCoordsForTheCurrentSearchContext() WebElement contextElement = mock(WebElement.class); when(contextElement.getLocation()).thenReturn(POINT); when(contextElement.getSize()).thenReturn(new Dimension(100, 50)); - doReturn(contextElement).when(uiContext).getSearchContext(); Coords coords = coordsProvider.ofElement(null, contextElement); Assertions.assertAll( () -> assertEquals(10, coords.getX()), @@ -109,13 +89,72 @@ void testCoordsIsMultipliedWithDpr(boolean downscale, Coords expectedCoords) WebElement element = mock(WebElement.class); when(element.getLocation()).thenReturn(POINT); when(element.getSize()).thenReturn(DIMENSION); - doReturn(element).when(uiContext).getSearchContext(); lenient().when(driverManager.getDpr()).thenReturn(2.0); WebDriver driver = mock(WebDriver.class); assertEquals(expectedCoords, coordsDecorator.ofElement(driver, element)); } + @Test + void shouldScaleElementCoordsRelativelyToContextCoords() + { + WebElement contextElement = mock(WebElement.class); + mockCoords(contextElement, 0, 138, 768, 1046); + when(uiContext.getOptionalSearchContext()).thenReturn(Optional.of(contextElement)); + WebElement element = mock(WebElement.class); + mockCoords(element, 0, 215, 768, 78); + when(driverManager.getStatusBarSize()).thenReturn(48); + when(driverManager.getDpr()).thenReturn(1.0810810327529907d); + + MobileAppCoordsProvider coordsDecorator = new MobileAppCoordsProvider(false, driverManager, uiContext); + Coords coords = coordsDecorator.ofElement(null, element); + Assertions.assertAll( + () -> assertEquals(0, coords.getX()), + () -> assertEquals(182, coords.getY()), + () -> assertEquals(831, coords.getWidth()), + () -> assertEquals(85, coords.getHeight())); + } + + @Test + void shouldNotAdjustCoordsForTheWebDriverSearchContext() + { + MobileAppCoordsProvider coordsDecorator = new MobileAppCoordsProvider(false, driverManager, uiContext); + WebElement element = mock(WebElement.class); + when(element.getLocation()).thenReturn(POINT); + when(element.getSize()).thenReturn(DIMENSION); + lenient().when(driverManager.getDpr()).thenReturn(2.0); + WebDriver webDriver = mock(WebDriver.class); + when(uiContext.getOptionalSearchContext()).thenReturn(Optional.of(webDriver)); + + assertEquals(new Coords(20, 20, 2, 2), coordsDecorator.ofElement(webDriver, element)); + } + + @Test + void shouldNotAdjustCoordsIfElementIsSearchContextElement() + { + MobileAppCoordsProvider coordsDecorator = new MobileAppCoordsProvider(false, driverManager, uiContext); + WebElement element = mock(WebElement.class); + when(element.getLocation()).thenReturn(POINT); + when(element.getSize()).thenReturn(DIMENSION); + lenient().when(driverManager.getDpr()).thenReturn(2.0); + when(uiContext.getOptionalSearchContext()).thenReturn(Optional.of(element)); + + WebDriver driver = mock(WebDriver.class); + assertEquals(new Coords(20, 20, 2, 2), coordsDecorator.ofElement(driver, element)); + } + + private void mockCoords(WebElement element, int x, int y, int w, int h) + { + Point point = mock(Point.class); + when(element.getLocation()).thenReturn(point); + when(point.getX()).thenReturn(x); + when(point.getY()).thenReturn(y); + Dimension dimension = mock(Dimension.class); + when(element.getSize()).thenReturn(dimension); + when(dimension.getWidth()).thenReturn(w); + when(dimension.getHeight()).thenReturn(h); + } + static Stream coordsSource() { return Stream.of(Arguments.of(true, new Coords(10, 10, 1, 1)), diff --git a/vividus-plugin-mobile-app/src/test/java/org/vividus/ui/mobileapp/screenshot/MobileAppScreenshotParametersFactoryTests.java b/vividus-plugin-mobile-app/src/test/java/org/vividus/ui/mobileapp/screenshot/MobileAppScreenshotParametersFactoryTests.java index a56c75411a..c86c03ad5a 100644 --- a/vividus-plugin-mobile-app/src/test/java/org/vividus/ui/mobileapp/screenshot/MobileAppScreenshotParametersFactoryTests.java +++ b/vividus-plugin-mobile-app/src/test/java/org/vividus/ui/mobileapp/screenshot/MobileAppScreenshotParametersFactoryTests.java @@ -19,17 +19,22 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.Mockito.mock; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.stream.Stream; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.InjectMocks; import org.mockito.junit.jupiter.MockitoExtension; +import org.vividus.selenium.screenshot.IgnoreStrategy; +import org.vividus.ui.action.search.Locator; import org.vividus.ui.screenshot.ScreenshotConfiguration; import org.vividus.ui.screenshot.ScreenshotParameters; import org.vividus.util.property.PropertyMappedCollection; @@ -59,6 +64,7 @@ void shouldCreateScreenshotConfiguration(int defaultFooter, Optional def defaultConfiguration.setNativeFooterToCut(defaultFooter); defaultConfiguration.setShootingStrategy(defaultStrategy); factory.setShootingStrategy(SIMPLE); + factory.setIgnoreStrategies(Map.of()); factory.setScreenshotConfigurations(new PropertyMappedCollection<>(Map.of(SIMPLE, defaultConfiguration))); ScreenshotConfiguration parameters = new ScreenshotConfiguration(); @@ -70,4 +76,24 @@ void shouldCreateScreenshotConfiguration(int defaultFooter, Optional def assertEquals(Optional.of(SIMPLE), configuration.getShootingStrategy()); assertEquals(TEN, configuration.getNativeFooterToCut()); } + + @Test + void shouldCreateScreenshotConfigurationWithIgnores() + { + ScreenshotConfiguration defaultConfiguration = new ScreenshotConfiguration(); + factory.setShootingStrategy(SIMPLE); + factory.setIgnoreStrategies(Map.of(IgnoreStrategy.ELEMENT, Set.of(), IgnoreStrategy.AREA, Set.of())); + factory.setScreenshotConfigurations(new PropertyMappedCollection<>(Map.of(SIMPLE, defaultConfiguration))); + + Locator locator = mock(Locator.class); + Map> ignores = Map.of( + IgnoreStrategy.ELEMENT, Set.of(locator), + IgnoreStrategy.AREA, Set.of(locator) + ); + Optional createdConfiguration = factory.create(ignores); + assertTrue(createdConfiguration.isPresent()); + + ScreenshotParameters configuration = createdConfiguration.get(); + assertEquals(ignores, configuration.getIgnoreStrategies()); + } } diff --git a/vividus-plugin-visual/src/main/java/org/vividus/visual/VisualCheckFactory.java b/vividus-plugin-visual/src/main/java/org/vividus/visual/VisualCheckFactory.java deleted file mode 100644 index b857ac01b9..0000000000 --- a/vividus-plugin-visual/src/main/java/org/vividus/visual/VisualCheckFactory.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2019-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.vividus.visual; - -import java.util.Optional; -import java.util.function.BiFunction; - -import org.vividus.ui.screenshot.ScreenshotConfiguration; -import org.vividus.ui.screenshot.ScreenshotParametersFactory; -import org.vividus.visual.model.VisualActionType; -import org.vividus.visual.model.VisualCheck; - -public class VisualCheckFactory extends AbstractVisualCheckFactory -{ - public VisualCheckFactory(ScreenshotParametersFactory screenshotParametersFactory) - { - super(screenshotParametersFactory); - } - - public VisualCheck create(String baselineName, VisualActionType actionType) - { - return create(baselineName, actionType, Optional.empty()); - } - - public VisualCheck create(String baselineName, VisualActionType actionType, - BiFunction checkFactory) - { - String indexedBaselineName = createIndexedBaseline(baselineName); - VisualCheck check = checkFactory.apply(indexedBaselineName, actionType); - withScreenshotConfiguration(check, Optional.empty()); - return check; - } - - public VisualCheck create(String baselineName, VisualActionType actionType, - Optional parameters) - { - String indexedBaselineName = createIndexedBaseline(baselineName); - VisualCheck check = new VisualCheck(indexedBaselineName, actionType); - withScreenshotConfiguration(check, parameters); - return check; - } -} diff --git a/vividus-plugin-visual/src/main/java/org/vividus/visual/VisualSteps.java b/vividus-plugin-visual/src/main/java/org/vividus/visual/VisualSteps.java index 12d78c4e92..bb144dc74a 100644 --- a/vividus-plugin-visual/src/main/java/org/vividus/visual/VisualSteps.java +++ b/vividus-plugin-visual/src/main/java/org/vividus/visual/VisualSteps.java @@ -22,6 +22,7 @@ import java.util.Optional; import java.util.OptionalDouble; import java.util.Set; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -29,20 +30,25 @@ import com.google.gson.reflect.TypeToken; +import org.apache.commons.lang3.Validate; import org.jbehave.core.annotations.When; import org.jbehave.core.model.ExamplesTable; +import org.jbehave.core.steps.ConvertedParameters; import org.jbehave.core.steps.Parameters; import org.vividus.reporter.event.IAttachmentPublisher; import org.vividus.resource.ResourceLoadException; +import org.vividus.selenium.screenshot.IgnoreStrategy; import org.vividus.softassert.ISoftAssert; import org.vividus.ui.action.search.Locator; import org.vividus.ui.context.IUiContext; import org.vividus.ui.screenshot.ScreenshotConfiguration; +import org.vividus.ui.screenshot.ScreenshotParameters; +import org.vividus.ui.screenshot.ScreenshotParametersFactory; import org.vividus.visual.engine.IVisualTestingEngine; import org.vividus.visual.model.VisualActionType; import org.vividus.visual.model.VisualCheck; import org.vividus.visual.model.VisualCheckResult; -import org.vividus.visual.screenshot.IgnoreStrategy; +import org.vividus.visual.screenshot.BaselineIndexer; import org.vividus.visual.steps.AbstractVisualSteps; public class VisualSteps extends AbstractVisualSteps @@ -53,14 +59,18 @@ public class VisualSteps extends AbstractVisualSteps private static final String REQUIRED_DIFF_PERCENTAGE_COLUMN_NAME = "REQUIRED_DIFF_PERCENTAGE"; private final IVisualTestingEngine visualTestingEngine; - private final VisualCheckFactory visualCheckFactory; + private final ScreenshotParametersFactory screenshotParametersFactory; + private final BaselineIndexer baselineIndexer; public VisualSteps(IUiContext uiContext, IAttachmentPublisher attachmentPublisher, - IVisualTestingEngine visualTestingEngine, ISoftAssert softAssert, VisualCheckFactory visualCheckFactory) + IVisualTestingEngine visualTestingEngine, ISoftAssert softAssert, + ScreenshotParametersFactory screenshotParametersFactory, + BaselineIndexer baselineIndexer) { super(uiContext, attachmentPublisher, softAssert); this.visualTestingEngine = visualTestingEngine; - this.visualCheckFactory = visualCheckFactory; + this.screenshotParametersFactory = screenshotParametersFactory; + this.baselineIndexer = baselineIndexer; } /** @@ -72,7 +82,7 @@ public VisualSteps(IUiContext uiContext, IAttachmentPublisher attachmentPublishe @When("I $actionType baseline with name `$name`") public void runVisualTests(VisualActionType actionType, String name) { - performVisualAction(() -> visualCheckFactory.create(name, actionType)); + performVisualAction(name, actionType, Optional.empty(), Optional.empty()); } /** @@ -85,16 +95,7 @@ public void runVisualTests(VisualActionType actionType, String name) @When(value = "I $actionType baseline with name `$name` using repository `$repository`", priority = 1) public void runVisualTests(VisualActionType actionType, String name, String repository) { - performVisualAction(withRepository(() -> visualCheckFactory.create(name, actionType), repository)); - } - - private Supplier withRepository(Supplier visualCheckProvider, String repository) - { - return () -> { - VisualCheck visualCheck = visualCheckProvider.get(); - visualCheck.setBaselineRepository(Optional.of(repository)); - return visualCheck; - }; + performVisualAction(name, actionType, Optional.of(repository), Optional.empty()); } /** @@ -111,7 +112,7 @@ private Supplier withRepository(Supplier visualCheckPr public void runVisualTests(VisualActionType actionType, String name, ScreenshotConfiguration screenshotConfiguration) { - performVisualAction(() -> visualCheckFactory.create(name, actionType, Optional.of(screenshotConfiguration))); + performVisualAction(name, actionType, Optional.empty(), Optional.of(screenshotConfiguration)); } /** @@ -130,25 +131,7 @@ public void runVisualTests(VisualActionType actionType, String name, public void runVisualTests(VisualActionType actionType, String name, String repository, ScreenshotConfiguration screenshotConfiguration) { - performVisualAction(withRepository(() -> visualCheckFactory.create(name, actionType, - Optional.of(screenshotConfiguration)), repository)); - } - - private void performVisualAction(Supplier visualCheckFactory) - { - execute(check -> { - try - { - return check.getAction() == VisualActionType.ESTABLISH - ? visualTestingEngine.establish(check) - : visualTestingEngine.compareAgainst(check); - } - catch (IOException | ResourceLoadException e) - { - getSoftAssert().recordFailedAssertion(e); - } - return null; - }, visualCheckFactory, "visual-comparison.ftl"); + performVisualAction(name, actionType, Optional.of(repository), Optional.of(screenshotConfiguration)); } /** @@ -164,7 +147,7 @@ private void performVisualAction(Supplier visualCheckFactory) @When("I $actionType baseline with name `$name` ignoring:$checkSettings") public void runVisualTests(VisualActionType actionType, String name, ExamplesTable checkSettings) { - runVisualTests(() -> visualCheckFactory.create(name, actionType), checkSettings); + performVisualAction(checkSettings, name, actionType, Optional.empty(), Optional.empty()); } /** @@ -183,7 +166,7 @@ public void runVisualTests(VisualActionType actionType, String name, ExamplesTab priority = 1) public void runVisualTests(VisualActionType actionType, String name, String repository, ExamplesTable checkSettings) { - runVisualTests(withRepository(() -> visualCheckFactory.create(name, actionType), repository), checkSettings); + performVisualAction(checkSettings, name, actionType, Optional.of(repository), Optional.empty()); } /** @@ -206,8 +189,7 @@ public void runVisualTests(VisualActionType actionType, String name, String repo public void runVisualTests(VisualActionType actionType, String name, ExamplesTable checkSettings, ScreenshotConfiguration screenshotConfiguration) { - runVisualTests(() -> visualCheckFactory.create(name, actionType, Optional.of(screenshotConfiguration)), - checkSettings); + performVisualAction(checkSettings, name, actionType, Optional.empty(), Optional.of(screenshotConfiguration)); } /** @@ -231,55 +213,95 @@ public void runVisualTests(VisualActionType actionType, String name, ExamplesTab public void runVisualTests(VisualActionType actionType, String name, String repository, ExamplesTable checkSettings, ScreenshotConfiguration screenshotConfiguration) { - runVisualTests( - withRepository(() -> visualCheckFactory.create(name, actionType, Optional.of(screenshotConfiguration)), - repository), checkSettings); + performVisualAction(checkSettings, name, actionType, Optional.of(repository), + Optional.of(screenshotConfiguration)); } - private void runVisualTests(Supplier visualCheckFactory, ExamplesTable checkSettings) + private void performVisualAction(ExamplesTable checkSettingsTable, String baselineName, VisualActionType actionType, + Optional baselineRepository, Optional screenshotConfiguration) { - int rowsSize = checkSettings.getRows().size(); - if (rowsSize != 1) - { - throw new IllegalArgumentException("Only one row of locators to ignore supported, actual: " - + rowsSize); - } - Parameters rowAsParameters = checkSettings.getRowAsParameters(0); - Map> toIgnore = Stream.of(IgnoreStrategy.values()) - .collect(Collectors.toMap(Function.identity(), - s -> getLocatorsSet(rowAsParameters, s))); + int rowsSize = checkSettingsTable.getRows().size(); + Validate.isTrue(rowsSize == 1, "Only one row of locators to ignore supported, actual: %s", rowsSize); + Parameters checkSettings = checkSettingsTable.getRowAsParameters(0); + + performVisualAction(baselineName, actionType, baselineRepository, screenshotConfiguration, checkSettings); + } + + private void performVisualAction(String baselineName, VisualActionType actionType, + Optional baselineRepository, Optional screenshotConfiguration) + { + performVisualAction(baselineName, actionType, baselineRepository, screenshotConfiguration, + new ConvertedParameters(Map.of(), null)); + } + + private void performVisualAction(String baselineName, VisualActionType actionType, + Optional baselineRepository, Optional screenshotConfiguration, + Parameters checkSettings) + { + Supplier visualCheckFactory = () -> { + Map> ignores = getIgnores(checkSettings); - performVisualAction(() -> { - VisualCheck visualCheck = visualCheckFactory.get(); - visualCheck.setElementsToIgnore(toIgnore); - setDiffPercentage(visualCheck, rowAsParameters); + Optional screenshotParameters; + if (screenshotConfiguration.isPresent()) + { + patchIgnores("ignores table", screenshotConfiguration.get(), ignores); + screenshotParameters = screenshotParametersFactory.create(screenshotConfiguration); + } + else + { + screenshotParameters = screenshotParametersFactory.create(ignores); + } + String indexedBaselineName = baselineIndexer.createIndexedBaseline(baselineName); + + VisualCheck visualCheck = new VisualCheck(indexedBaselineName, actionType); + visualCheck.setScreenshotParameters(screenshotParameters); + visualCheck.setBaselineRepository(baselineRepository); + setDiffPercentage(visualCheck, checkSettings); return visualCheck; - }); + }; + Function checkResultProvider = check -> { + try + { + return check.getAction() == VisualActionType.ESTABLISH + ? visualTestingEngine.establish(check) + : visualTestingEngine.compareAgainst(check); + } + catch (IOException | ResourceLoadException e) + { + getSoftAssert().recordFailedAssertion(e); + } + return null; + }; + execute(visualCheckFactory, checkResultProvider, "visual-comparison.ftl"); + } + + private Map> getIgnores(Parameters checkParameters) + { + return Stream.of(IgnoreStrategy.values()).collect(Collectors.toMap(Function.identity(), + s -> checkParameters.valueAs(s.name(), SET_BY, Set.of()))); } private void setDiffPercentage(VisualCheck visualCheck, Parameters rowAsParameters) { if (visualCheck.getAction() == VisualActionType.CHECK_INEQUALITY_AGAINST) { - visualCheck.setRequiredDiffPercentage(getParameter(rowAsParameters, REQUIRED_DIFF_PERCENTAGE_COLUMN_NAME)); + configureThresholdIfPresent(rowAsParameters, REQUIRED_DIFF_PERCENTAGE_COLUMN_NAME, + visualCheck::setRequiredDiffPercentage); } else { - visualCheck.setAcceptableDiffPercentage(getParameter(rowAsParameters, - ACCEPTABLE_DIFF_PERCENTAGE_COLUMN_NAME)); + configureThresholdIfPresent(rowAsParameters, ACCEPTABLE_DIFF_PERCENTAGE_COLUMN_NAME, + visualCheck::setAcceptableDiffPercentage); } } - private OptionalDouble getParameter(Parameters rowAsParameters, String paramterName) - { - return rowAsParameters.values().containsKey(paramterName) - ? OptionalDouble.of(rowAsParameters.valueAs(paramterName, Double.TYPE)) - : OptionalDouble.empty(); - } - - private Set getLocatorsSet(Parameters rowAsParameters, IgnoreStrategy s) + private void configureThresholdIfPresent(Parameters rowAsParameters, String parameterName, + Consumer thresholdSetter) { - return rowAsParameters.valueAs(s.name(), SET_BY, Set.of()); + if (rowAsParameters.values().containsKey(parameterName)) + { + thresholdSetter.accept(OptionalDouble.of(rowAsParameters.valueAs(parameterName, Double.TYPE))); + } } @Override diff --git a/vividus-plugin-visual/src/main/java/org/vividus/visual/engine/VisualTestingEngine.java b/vividus-plugin-visual/src/main/java/org/vividus/visual/engine/VisualTestingEngine.java index b0f0848d3d..46a81dfff3 100644 --- a/vividus-plugin-visual/src/main/java/org/vividus/visual/engine/VisualTestingEngine.java +++ b/vividus-plugin-visual/src/main/java/org/vividus/visual/engine/VisualTestingEngine.java @@ -26,10 +26,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.vividus.selenium.screenshot.AshotScreenshotTaker; +import org.vividus.ui.screenshot.ScreenshotParameters; import org.vividus.visual.model.VisualActionType; import org.vividus.visual.model.VisualCheck; import org.vividus.visual.model.VisualCheckResult; -import org.vividus.visual.screenshot.ScreenshotProvider; import ru.yandex.qatools.ashot.Screenshot; import ru.yandex.qatools.ashot.comparison.ImageDiff; @@ -42,7 +43,7 @@ public class VisualTestingEngine implements IVisualTestingEngine private static final int ONE_HUNDRED = 100; private static final int SCALE = 3; - private final ScreenshotProvider screenshotProvider; + private final AshotScreenshotTaker ashotScreenshotTaker; private final DiffMarkupPolicyFactory diffMarkupPolicyFactory; private final Map baselineRepositories; @@ -51,10 +52,10 @@ public class VisualTestingEngine implements IVisualTestingEngine private boolean overrideBaselines; private String baselineRepository; - public VisualTestingEngine(ScreenshotProvider screenshotProvider, DiffMarkupPolicyFactory diffMarkupPolicyFactory, - Map baselineRepositories) + public VisualTestingEngine(AshotScreenshotTaker ashotScreenshotTaker, + DiffMarkupPolicyFactory diffMarkupPolicyFactory, Map baselineRepositories) { - this.screenshotProvider = screenshotProvider; + this.ashotScreenshotTaker = ashotScreenshotTaker; this.diffMarkupPolicyFactory = diffMarkupPolicyFactory; this.baselineRepositories = baselineRepositories; } @@ -71,7 +72,8 @@ public VisualCheckResult establish(VisualCheck visualCheck) throws IOException private Screenshot getCheckpointScreenshot(VisualCheck visualCheck) { - return screenshotProvider.take(visualCheck); + return ashotScreenshotTaker.takeAshotScreenshot(visualCheck.getSearchContext(), + visualCheck.getScreenshotParameters()); } @Override diff --git a/vividus-plugin-visual/src/main/resources/spring.xml b/vividus-plugin-visual/src/main/resources/spring.xml index c44e8b1581..724597d84b 100644 --- a/vividus-plugin-visual/src/main/resources/spring.xml +++ b/vividus-plugin-visual/src/main/resources/spring.xml @@ -6,9 +6,12 @@ http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd" default-lazy-init="true" profile="web,mobile_app"> - + + + + diff --git a/vividus-plugin-visual/src/test/java/org/vividus/visual/VisualCheckFactoryTests.java b/vividus-plugin-visual/src/test/java/org/vividus/visual/VisualCheckFactoryTests.java deleted file mode 100644 index 3f60cb29c4..0000000000 --- a/vividus-plugin-visual/src/test/java/org/vividus/visual/VisualCheckFactoryTests.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2019-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.vividus.visual; - -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.Map; -import java.util.Optional; -import java.util.OptionalDouble; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.vividus.ui.screenshot.ScreenshotConfiguration; -import org.vividus.ui.screenshot.ScreenshotParameters; -import org.vividus.ui.screenshot.ScreenshotParametersFactory; -import org.vividus.visual.model.VisualActionType; -import org.vividus.visual.model.VisualCheck; -import org.vividus.visual.screenshot.IScreenshotIndexer; - -@ExtendWith(MockitoExtension.class) -class VisualCheckFactoryTests -{ - private static final String NAME = "name"; - private static final String INDEXED_NAME = NAME + " [0]"; - - @Mock private ScreenshotParametersFactory screenshotParametersFactory; - @InjectMocks private VisualCheckFactory visualCheckFactory; - - @Test - void shouldCreateVisualCheckWithoutIndexedBaseline() - { - visualCheckFactory.setScreenshotIndexer(Optional.empty()); - visualCheckFactory.setIndexers(Map.of()); - VisualCheck check = visualCheckFactory.create(NAME, VisualActionType.COMPARE_AGAINST); - assertAll( - () -> assertEquals(NAME, check.getBaselineName()), - () -> assertEquals(VisualActionType.COMPARE_AGAINST, check.getAction()), - () -> assertEquals(OptionalDouble.empty(), check.getAcceptableDiffPercentage()), - () -> assertEquals(Map.of(), check.getElementsToIgnore()), - () -> assertEquals(Optional.empty(), check.getScreenshotParameters())); - } - - @Test - void shouldCreateVisualCheckFromInputFactoryWithIndexedBaseline() - { - mockIndexer(); - VisualCheck check = visualCheckFactory.create(NAME, VisualActionType.COMPARE_AGAINST, VisualCheck::new); - assertAll( - () -> assertEquals(INDEXED_NAME, check.getBaselineName()), - () -> assertEquals(VisualActionType.COMPARE_AGAINST, check.getAction()) - ); - } - - @Test - void shouldCreateVisualCheckWithIndexedBaseline() - { - mockIndexer(); - VisualCheck check = visualCheckFactory.create(NAME, VisualActionType.COMPARE_AGAINST); - assertAll( - () -> assertEquals(INDEXED_NAME, check.getBaselineName()), - () -> assertEquals(VisualActionType.COMPARE_AGAINST, check.getAction()), - () -> assertEquals(OptionalDouble.empty(), check.getAcceptableDiffPercentage()), - () -> assertEquals(Map.of(), check.getElementsToIgnore()), - () -> assertEquals(Optional.empty(), check.getScreenshotParameters())); - } - - private void mockIndexer() - { - visualCheckFactory.setScreenshotIndexer(Optional.of(NAME)); - IScreenshotIndexer indexer = mock(IScreenshotIndexer.class); - visualCheckFactory.setIndexers(Map.of(NAME, indexer)); - when(indexer.index(NAME)).thenReturn(INDEXED_NAME); - } - - @Test - void shouldCreateVisualCheckWithScreenshotConfiguration() - { - visualCheckFactory.setScreenshotIndexer(Optional.empty()); - visualCheckFactory.setIndexers(Map.of()); - ScreenshotConfiguration screenshotConfiguration = mock(ScreenshotConfiguration.class); - ScreenshotParameters screenshotParameters = mock(ScreenshotParameters.class); - when(screenshotParametersFactory.create(Optional.of(screenshotConfiguration))) - .thenReturn(Optional.of(screenshotParameters)); - VisualCheck check = visualCheckFactory.create(NAME, VisualActionType.COMPARE_AGAINST, - Optional.of(screenshotConfiguration)); - assertAll( - () -> assertEquals(NAME, check.getBaselineName()), - () -> assertEquals(VisualActionType.COMPARE_AGAINST, check.getAction()), - () -> assertEquals(OptionalDouble.empty(), check.getAcceptableDiffPercentage()), - () -> assertEquals(Map.of(), check.getElementsToIgnore()), - () -> assertEquals(Optional.of(screenshotParameters), check.getScreenshotParameters())); - } -} diff --git a/vividus-plugin-visual/src/test/java/org/vividus/visual/VisualStepsTests.java b/vividus-plugin-visual/src/test/java/org/vividus/visual/VisualStepsTests.java index 060ef3c849..810ec8ffc3 100644 --- a/vividus-plugin-visual/src/test/java/org/vividus/visual/VisualStepsTests.java +++ b/vividus-plugin-visual/src/test/java/org/vividus/visual/VisualStepsTests.java @@ -49,23 +49,27 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.openqa.selenium.SearchContext; import org.vividus.reporter.event.IAttachmentPublisher; import org.vividus.resource.ResourceLoadException; +import org.vividus.selenium.screenshot.IgnoreStrategy; import org.vividus.softassert.ISoftAssert; import org.vividus.ui.action.search.Locator; import org.vividus.ui.context.IUiContext; import org.vividus.ui.screenshot.ScreenshotConfiguration; +import org.vividus.ui.screenshot.ScreenshotParameters; import org.vividus.ui.screenshot.ScreenshotParametersFactory; import org.vividus.ui.web.action.search.WebLocatorType; import org.vividus.visual.engine.IVisualTestingEngine; import org.vividus.visual.model.VisualActionType; import org.vividus.visual.model.VisualCheck; import org.vividus.visual.model.VisualCheckResult; -import org.vividus.visual.screenshot.IgnoreStrategy; +import org.vividus.visual.screenshot.BaselineIndexer; @ExtendWith(MockitoExtension.class) class VisualStepsTests @@ -91,46 +95,43 @@ class VisualStepsTests @Mock private ISoftAssert softAssert; @Mock private IAttachmentPublisher attachmentPublisher; @Mock private VisualCheckResult visualCheckResult; - @Mock private VisualCheckFactory visualCheckFactory; @Mock private IUiContext uiContext; + @Mock private BaselineIndexer baselineIndexer; + @Captor private ArgumentCaptor visualCheckCaptor; @InjectMocks private VisualSteps visualSteps; - private VisualCheckFactory factory; - @BeforeEach void init() { - factory = new VisualCheckFactory(screenshotParametersFactory); + lenient().when(baselineIndexer.createIndexedBaseline(BASELINE)).thenReturn(BASELINE); lenient().when(screenshotParametersFactory.create(Optional.empty())).thenReturn(Optional.empty()); - factory.setScreenshotIndexer(Optional.empty()); - factory.setIndexers(Map.of()); + lenient().when(screenshotParametersFactory.create(Map.of())).thenReturn(Optional.empty()); } @ParameterizedTest @CsvSource({"COMPARE_AGAINST", "CHECK_INEQUALITY_AGAINST"}) void shouldAssertCheckResultForCompareAgainstActionAndPublishAttachment(VisualActionType action) throws IOException { - VisualCheck visualCheck = mockVisualCheckFactory(action); mockUiContext(); - when(visualTestingEngine.compareAgainst(visualCheck)).thenReturn(visualCheckResult); + when(visualTestingEngine.compareAgainst(visualCheckCaptor.capture())).thenReturn(visualCheckResult); mockCheckResult(); visualSteps.runVisualTests(action, BASELINE); + validateVisualCheck(visualCheckCaptor.getValue(), action); verify(softAssert).assertTrue(VISUAL_CHECK_PASSED, false); - assertEquals(Map.of(), visualCheck.getElementsToIgnore()); verifyCheckResultPublish(); } @Test void shouldPerformVisualCheckWithBaselineRepository() throws IOException { - VisualCheck visualCheck = mockVisualCheckFactory(VisualActionType.ESTABLISH); mockUiContext(); - when(visualTestingEngine.establish(visualCheck)).thenReturn(visualCheckResult); + when(visualTestingEngine.establish(visualCheckCaptor.capture())).thenReturn(visualCheckResult); mockCheckResult(); visualSteps.runVisualTests(VisualActionType.ESTABLISH, BASELINE, FILESYSTEM); verify(softAssert).assertTrue(VISUAL_CHECK_PASSED, false); - assertEquals(Map.of(), visualCheck.getElementsToIgnore()); + VisualCheck visualCheck = visualCheckCaptor.getValue(); assertEquals(Optional.of(FILESYSTEM), visualCheck.getBaselineRepository()); + validateVisualCheck(visualCheck, VisualActionType.ESTABLISH); verifyCheckResultPublish(); } @@ -146,14 +147,13 @@ void shouldPerformVisualCheckWithCustomConfiguration() throws IOException VisualActionType compareAgainst = VisualActionType.COMPARE_AGAINST; mockUiContext(); ScreenshotConfiguration screenshotConfiguration = mock(ScreenshotConfiguration.class); - VisualCheck visualCheck = factory.create(BASELINE, compareAgainst); - when(visualCheckFactory.create(BASELINE, compareAgainst, Optional.of(screenshotConfiguration))) - .thenReturn(visualCheck); - when(visualTestingEngine.compareAgainst(visualCheck)).thenReturn(visualCheckResult); + Optional screenshotParameters = Optional.of(mock(ScreenshotParameters.class)); + when(screenshotParametersFactory.create(Optional.of(screenshotConfiguration))).thenReturn(screenshotParameters); + when(visualTestingEngine.compareAgainst(visualCheckCaptor.capture())).thenReturn(visualCheckResult); mockCheckResult(); visualSteps.runVisualTests(compareAgainst, BASELINE, screenshotConfiguration); + validateVisualCheck(visualCheckCaptor.getValue(), compareAgainst); verify(softAssert).assertTrue(VISUAL_CHECK_PASSED, false); - assertEquals(Map.of(), visualCheck.getElementsToIgnore()); verifyCheckResultPublish(); } @@ -163,29 +163,28 @@ void shouldPerformVisualCheckWithCustomConfigurationAndBaselineRepository() thro VisualActionType compareAgainst = VisualActionType.COMPARE_AGAINST; mockUiContext(); ScreenshotConfiguration screenshotConfiguration = mock(ScreenshotConfiguration.class); - VisualCheck visualCheck = factory.create(BASELINE, compareAgainst); - when(visualCheckFactory.create(BASELINE, compareAgainst, Optional.of(screenshotConfiguration))) - .thenReturn(visualCheck); - when(visualTestingEngine.compareAgainst(visualCheck)).thenReturn(visualCheckResult); + Optional screenshotParameters = Optional.of(mock(ScreenshotParameters.class)); + when(screenshotParametersFactory.create(Optional.of(screenshotConfiguration))).thenReturn(screenshotParameters); + when(visualTestingEngine.compareAgainst(visualCheckCaptor.capture())).thenReturn(visualCheckResult); mockCheckResult(); visualSteps.runVisualTests(compareAgainst, BASELINE, FILESYSTEM, screenshotConfiguration); verify(softAssert).assertTrue(VISUAL_CHECK_PASSED, false); - assertEquals(Map.of(), visualCheck.getElementsToIgnore()); + VisualCheck visualCheck = visualCheckCaptor.getValue(); assertEquals(Optional.of(FILESYSTEM), visualCheck.getBaselineRepository()); + validateVisualCheck(visualCheck, compareAgainst); verifyCheckResultPublish(); } @Test void shouldRecordFailedAssertionInCaseOfMissingBaseline() throws IOException { - VisualCheck visualCheck = mockVisualCheckFactory(VisualActionType.COMPARE_AGAINST); mockUiContext(); - when(visualTestingEngine.compareAgainst(visualCheck)).thenReturn(visualCheckResult); + when(visualTestingEngine.compareAgainst(visualCheckCaptor.capture())).thenReturn(visualCheckResult); when(visualCheckResult.getBaselineName()).thenReturn(BASELINE); visualSteps.runVisualTests(VisualActionType.COMPARE_AGAINST, BASELINE); verify(softAssert, never()).assertTrue(VISUAL_CHECK_PASSED, false); verify(softAssert).recordFailedAssertion("Unable to find baseline with name: baseline"); - assertEquals(Map.of(), visualCheck.getElementsToIgnore()); + validateVisualCheck(visualCheckCaptor.getValue(), VisualActionType.COMPARE_AGAINST); verifyCheckResultPublish(); } @@ -214,14 +213,17 @@ void shouldAssertCheckResultAndUseStepLevelSettings(VisualActionType actionType, Set elementsToIgnore = Set.of(A_LOCATOR); Set areasToIgnore = Set.of(DIV_LOCATOR); mockRow(row, elementsToIgnore, areasToIgnore, 50, columnName); - VisualCheck visualCheck = mockVisualCheckFactory(actionType); - when(visualTestingEngine.compareAgainst(visualCheck)).thenReturn(visualCheckResult); + Map> ignores = Map.of(IgnoreStrategy.ELEMENT, elementsToIgnore, + IgnoreStrategy.AREA, areasToIgnore); + Optional screenshotParameters = Optional.of(mock(ScreenshotParameters.class)); + when(screenshotParametersFactory.create(ignores)).thenReturn(screenshotParameters); + when(visualTestingEngine.compareAgainst(visualCheckCaptor.capture())).thenReturn(visualCheckResult); mockCheckResult(); visualSteps.runVisualTests(actionType, BASELINE, table); verify(softAssert).assertTrue(VISUAL_CHECK_PASSED, false); + VisualCheck visualCheck = visualCheckCaptor.getValue(); assertEquals(OptionalDouble.of(50), diffValueExtractor.apply(visualCheck)); - assertEquals(Map.of(IgnoreStrategy.ELEMENT, elementsToIgnore, IgnoreStrategy.AREA, areasToIgnore), - visualCheck.getElementsToIgnore()); + validateVisualCheck(visualCheck, actionType); verifyCheckResultPublish(); } @@ -236,15 +238,19 @@ void shouldAssertCheckResultUsingBaselineRepositoryAndUseStepLevelSettings() thr Set elementsToIgnore = Set.of(A_LOCATOR); Set areasToIgnore = Set.of(DIV_LOCATOR); mockRow(row, elementsToIgnore, areasToIgnore, 50, ACCEPTABLE_DIFF_PERCENTAGE); - VisualCheck visualCheck = mockVisualCheckFactory(VisualActionType.COMPARE_AGAINST); - when(visualTestingEngine.compareAgainst(visualCheck)).thenReturn(visualCheckResult); + Map> ignores = Map.of(IgnoreStrategy.ELEMENT, elementsToIgnore, + IgnoreStrategy.AREA, areasToIgnore); + Optional screenshotParameters = Optional.of(mock(ScreenshotParameters.class)); + when(screenshotParametersFactory.create(ignores)).thenReturn(screenshotParameters); + + when(visualTestingEngine.compareAgainst(visualCheckCaptor.capture())).thenReturn(visualCheckResult); mockCheckResult(); visualSteps.runVisualTests(VisualActionType.COMPARE_AGAINST, BASELINE, FILESYSTEM, table); verify(softAssert).assertTrue(VISUAL_CHECK_PASSED, false); + VisualCheck visualCheck = visualCheckCaptor.getValue(); assertEquals(OptionalDouble.of(50), visualCheck.getAcceptableDiffPercentage()); assertEquals(Optional.of(FILESYSTEM), visualCheck.getBaselineRepository()); - assertEquals(Map.of(IgnoreStrategy.ELEMENT, elementsToIgnore, IgnoreStrategy.AREA, areasToIgnore), - visualCheck.getElementsToIgnore()); + validateVisualCheck(visualCheck, VisualActionType.COMPARE_AGAINST); verifyCheckResultPublish(); } @@ -260,17 +266,16 @@ void shouldRunVisualTestWithStepLevelExclusionsAndCustomScreenshotConfiguration( Set areasToIgnore = Set.of(DIV_LOCATOR); mockRow(row, elementsToIgnore, areasToIgnore); ScreenshotConfiguration screenshotConfiguration = mock(ScreenshotConfiguration.class); + Optional screenshotParameters = Optional.of(mock(ScreenshotParameters.class)); + when(screenshotParametersFactory.create(Optional.of(screenshotConfiguration))).thenReturn(screenshotParameters); VisualActionType compareAgainst = VisualActionType.COMPARE_AGAINST; - VisualCheck visualCheck = factory.create(BASELINE, compareAgainst); - when(visualCheckFactory.create(BASELINE, compareAgainst, Optional.of(screenshotConfiguration))) - .thenReturn(visualCheck); - when(visualTestingEngine.compareAgainst(visualCheck)).thenReturn(visualCheckResult); + when(visualTestingEngine.compareAgainst(visualCheckCaptor.capture())).thenReturn(visualCheckResult); mockCheckResult(); visualSteps.runVisualTests(compareAgainst, BASELINE, table, screenshotConfiguration); verify(softAssert).assertTrue(VISUAL_CHECK_PASSED, false); + VisualCheck visualCheck = visualCheckCaptor.getValue(); assertEquals(OptionalDouble.empty(), visualCheck.getRequiredDiffPercentage()); - assertEquals(Map.of(IgnoreStrategy.ELEMENT, elementsToIgnore, IgnoreStrategy.AREA, areasToIgnore), - visualCheck.getElementsToIgnore()); + validateVisualCheck(visualCheck, compareAgainst); verifyCheckResultPublish(); } @@ -287,18 +292,17 @@ void shouldRunVisualTestWithBaselineRepositoryAndStepLevelExclusionsAndCustomScr Set areasToIgnore = Set.of(DIV_LOCATOR); mockRow(row, elementsToIgnore, areasToIgnore); ScreenshotConfiguration screenshotConfiguration = mock(ScreenshotConfiguration.class); + Optional screenshotParameters = Optional.of(mock(ScreenshotParameters.class)); + when(screenshotParametersFactory.create(Optional.of(screenshotConfiguration))).thenReturn(screenshotParameters); VisualActionType compareAgainst = VisualActionType.COMPARE_AGAINST; - VisualCheck visualCheck = factory.create(BASELINE, compareAgainst); - when(visualCheckFactory.create(BASELINE, compareAgainst, Optional.of(screenshotConfiguration))) - .thenReturn(visualCheck); - when(visualTestingEngine.compareAgainst(visualCheck)).thenReturn(visualCheckResult); + when(visualTestingEngine.compareAgainst(visualCheckCaptor.capture())).thenReturn(visualCheckResult); mockCheckResult(); visualSteps.runVisualTests(compareAgainst, BASELINE, FILESYSTEM, table, screenshotConfiguration); verify(softAssert).assertTrue(VISUAL_CHECK_PASSED, false); + VisualCheck visualCheck = visualCheckCaptor.getValue(); assertEquals(OptionalDouble.empty(), visualCheck.getRequiredDiffPercentage()); assertEquals(Optional.of(FILESYSTEM), visualCheck.getBaselineRepository()); - assertEquals(Map.of(IgnoreStrategy.ELEMENT, elementsToIgnore, IgnoreStrategy.AREA, areasToIgnore), - visualCheck.getElementsToIgnore()); + validateVisualCheck(visualCheck, compareAgainst); verifyCheckResultPublish(); } @@ -307,13 +311,6 @@ private void mockCheckResult() when(visualCheckResult.getBaseline()).thenReturn(StringUtils.EMPTY); } - private VisualCheck mockVisualCheckFactory(VisualActionType actionType) - { - VisualCheck visualCheck = factory.create(BASELINE, actionType); - when(visualCheckFactory.create(BASELINE, actionType)).thenReturn(visualCheck); - return visualCheck; - } - @Test void shouldThrowExceptionIfTableHasMoreThanOneRow() { @@ -353,13 +350,12 @@ private static void mockGettingValue(Parameters row, String name, Set r void shouldNotAssertResultForEstablishAction() throws IOException { mockUiContext(); - VisualCheck visualCheck = mockVisualCheckFactory(VisualActionType.ESTABLISH); - when(visualTestingEngine.establish(visualCheck)).thenReturn(visualCheckResult); + when(visualTestingEngine.establish(visualCheckCaptor.capture())).thenReturn(visualCheckResult); when(visualCheckResult.getActionType()).thenReturn(VisualActionType.ESTABLISH); visualSteps.runVisualTests(VisualActionType.ESTABLISH, BASELINE); verifyNoMoreInteractions(softAssert); verifyCheckResultPublish(); - assertEquals(Map.of(), visualCheck.getElementsToIgnore()); + validateVisualCheck(visualCheckCaptor.getValue(), VisualActionType.ESTABLISH); } @Test @@ -367,7 +363,7 @@ void shouldDoNothingOnMissingSearchContext() { when(uiContext.getOptionalSearchContext()).thenReturn(Optional.empty()); visualSteps.runVisualTests(VisualActionType.ESTABLISH, BASELINE); - verifyNoInteractions(visualCheckFactory, visualTestingEngine, attachmentPublisher); + verifyNoInteractions(visualTestingEngine, attachmentPublisher); } static Stream exceptionsToCatch() @@ -385,12 +381,18 @@ void shouldRecordExceptions(Exception exception) throws IOException private void shouldRecordException(Exception exception) throws IOException { - VisualCheck visualCheck = mockVisualCheckFactory(VisualActionType.ESTABLISH); - when(visualTestingEngine.establish(visualCheck)).thenThrow(exception); + when(visualTestingEngine.establish(visualCheckCaptor.capture())).thenThrow(exception); visualSteps.runVisualTests(VisualActionType.ESTABLISH, BASELINE); verify(softAssert).recordFailedAssertion(exception); verifyNoInteractions(attachmentPublisher); verifyNoMoreInteractions(softAssert); + validateVisualCheck(visualCheckCaptor.getValue(), VisualActionType.ESTABLISH); + } + + private void validateVisualCheck(VisualCheck visualCheck, VisualActionType type) + { + assertEquals(BASELINE, visualCheck.getBaselineName()); + assertEquals(type, visualCheck.getAction()); } private void verifyCheckResultPublish() diff --git a/vividus-plugin-visual/src/test/java/org/vividus/visual/engine/VisualTestingEngineTests.java b/vividus-plugin-visual/src/test/java/org/vividus/visual/engine/VisualTestingEngineTests.java index 851a8a2f02..94ebd78358 100644 --- a/vividus-plugin-visual/src/test/java/org/vividus/visual/engine/VisualTestingEngineTests.java +++ b/vividus-plugin-visual/src/test/java/org/vividus/visual/engine/VisualTestingEngineTests.java @@ -48,7 +48,6 @@ import com.github.valfirst.slf4jtest.TestLoggerFactoryExtension; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; @@ -56,14 +55,12 @@ import org.mockito.Mock; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; -import org.vividus.ui.screenshot.ScreenshotConfiguration; -import org.vividus.ui.screenshot.ScreenshotParametersFactory; +import org.vividus.selenium.screenshot.AshotScreenshotTaker; +import org.vividus.ui.screenshot.ScreenshotParameters; import org.vividus.util.ResourceUtils; -import org.vividus.visual.VisualCheckFactory; import org.vividus.visual.model.VisualActionType; import org.vividus.visual.model.VisualCheck; import org.vividus.visual.model.VisualCheckResult; -import org.vividus.visual.screenshot.ScreenshotProvider; import ru.yandex.qatools.ashot.Screenshot; @@ -91,24 +88,12 @@ class VisualTestingEngineTests private final TestLogger testLogger = TestLoggerFactory.getTestLogger(VisualTestingEngine.class); - @Mock private ScreenshotParametersFactory screenshotParametersFactory; @Mock private BaselineRepository baselineRepository; - @Mock private ScreenshotProvider screenshotProvider; + @Mock private AshotScreenshotTaker ashotScreenshotTaker; @Spy private DiffMarkupPolicyFactory diffMarkupPolicyFactory; private VisualTestingEngine visualTestingEngine; - private VisualCheckFactory factory; - - @BeforeEach - void init() - { - factory = new VisualCheckFactory(screenshotParametersFactory); - when(screenshotParametersFactory.create(Optional.empty())).thenReturn(Optional.empty()); - factory.setScreenshotIndexer(Optional.empty()); - factory.setIndexers(Map.of()); - } - void initObjectUnderTest() { initObjectUnderTest(Map.of(FILESYSTEM, baselineRepository)); @@ -116,7 +101,7 @@ void initObjectUnderTest() void initObjectUnderTest(Map baselineRepositories) { - visualTestingEngine = new VisualTestingEngine(screenshotProvider, diffMarkupPolicyFactory, + visualTestingEngine = new VisualTestingEngine(ashotScreenshotTaker, diffMarkupPolicyFactory, baselineRepositories); visualTestingEngine.setAcceptableDiffPercentage(0.0d); visualTestingEngine.setBaselineRepository(FILESYSTEM); @@ -142,7 +127,9 @@ void shouldReturnOnlyCheckpointForEstablishAction() throws IOException private VisualCheck createVisualCheck(VisualActionType actionType) { - return factory.create(BASELINE, actionType); + VisualCheck check = new VisualCheck(BASELINE, actionType); + check.setScreenshotParameters(Optional.empty()); + return check; } @ParameterizedTest @@ -201,7 +188,7 @@ void shouldReturnVisualCheckResultWithBaselineAndCheckpointUsingAcceptableDiffPe { initObjectUnderTest(); when(baselineRepository.getBaseline(BASELINE)).thenReturn(Optional.of(new Screenshot(loadImage(BASELINE)))); - VisualCheck visualCheck = factory.create(BASELINE, VisualActionType.COMPARE_AGAINST); + VisualCheck visualCheck = createVisualCheck(VisualActionType.COMPARE_AGAINST); visualCheck.setAcceptableDiffPercentage(OptionalDouble.of(50)); mockGetCheckpointScreenshot(visualCheck); VisualCheckResult checkResult = visualTestingEngine.compareAgainst(visualCheck); @@ -313,7 +300,8 @@ private BufferedImage mockGetCheckpointScreenshot(VisualCheck visualCheck, Strin { BufferedImage image = loadImage(imageName); Screenshot screenshot = new Screenshot(image); - when(screenshotProvider.take(visualCheck)).thenReturn(screenshot); + when(ashotScreenshotTaker.takeAshotScreenshot(visualCheck.getSearchContext(), + visualCheck.getScreenshotParameters())).thenReturn(screenshot); return image; } diff --git a/vividus-plugin-visual/src/test/java/org/vividus/visual/model/VisualCheckTests.java b/vividus-plugin-visual/src/test/java/org/vividus/visual/model/VisualCheckTests.java index 739f8e9f2c..3f5f929255 100644 --- a/vividus-plugin-visual/src/test/java/org/vividus/visual/model/VisualCheckTests.java +++ b/vividus-plugin-visual/src/test/java/org/vividus/visual/model/VisualCheckTests.java @@ -18,7 +18,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import java.util.Map; import java.util.Optional; import java.util.OptionalDouble; @@ -36,7 +35,6 @@ void shouldHaveDefaultParameters() Assertions.assertAll( () -> assertEquals(OptionalDouble.empty(), visualCheck.getAcceptableDiffPercentage()), () -> assertEquals(OptionalDouble.empty(), visualCheck.getRequiredDiffPercentage()), - () -> assertEquals(Map.of(), visualCheck.getElementsToIgnore()), () -> assertEquals(Optional.empty(), visualCheck.getScreenshotParameters())); } diff --git a/vividus-plugin-web-app/src/main/java/org/vividus/selenium/screenshot/ScrollBarHidingCoordsProviderDecorator.java b/vividus-plugin-web-app/src/main/java/org/vividus/selenium/screenshot/ScrollBarHidingCoordsProviderDecorator.java new file mode 100644 index 0000000000..e22130f24f --- /dev/null +++ b/vividus-plugin-web-app/src/main/java/org/vividus/selenium/screenshot/ScrollBarHidingCoordsProviderDecorator.java @@ -0,0 +1,43 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.vividus.selenium.screenshot; + +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; + +import ru.yandex.qatools.ashot.coordinates.Coords; +import ru.yandex.qatools.ashot.coordinates.CoordsProvider; + +public class ScrollBarHidingCoordsProviderDecorator extends CoordsProvider +{ + private static final long serialVersionUID = -4309766535331129861L; + + private final CoordsProvider coordsProvider; + private final transient IScrollbarHandler scrollbarHandler; + + public ScrollBarHidingCoordsProviderDecorator(CoordsProvider coordsProvider, IScrollbarHandler scrollbarHandler) + { + this.coordsProvider = coordsProvider; + this.scrollbarHandler = scrollbarHandler; + } + + @Override + public Coords ofElement(WebDriver driver, WebElement element) + { + return scrollbarHandler.performActionWithHiddenScrollbars(() -> coordsProvider.ofElement(driver, element)); + } +} diff --git a/vividus-plugin-web-app/src/main/java/org/vividus/selenium/screenshot/WebAshotFactory.java b/vividus-plugin-web-app/src/main/java/org/vividus/selenium/screenshot/WebAshotFactory.java index 393ac3cfdf..43a2c484c9 100644 --- a/vividus-plugin-web-app/src/main/java/org/vividus/selenium/screenshot/WebAshotFactory.java +++ b/vividus-plugin-web-app/src/main/java/org/vividus/selenium/screenshot/WebAshotFactory.java @@ -18,6 +18,7 @@ import java.util.Optional; +import org.openqa.selenium.WebElement; import org.vividus.selenium.screenshot.strategies.AdjustingScrollableElementAwareViewportPastingDecorator; import org.vividus.selenium.screenshot.strategies.AdjustingViewportPastingDecorator; import org.vividus.selenium.screenshot.strategies.ScreenshotShootingStrategy; @@ -28,7 +29,9 @@ import org.vividus.ui.web.screenshot.WebScreenshotParameters; import ru.yandex.qatools.ashot.AShot; +import ru.yandex.qatools.ashot.coordinates.CoordsProvider; import ru.yandex.qatools.ashot.shooting.DebuggingViewportPastingDecorator; +import ru.yandex.qatools.ashot.shooting.ScrollbarHidingDecorator; import ru.yandex.qatools.ashot.shooting.ShootingStrategy; public class WebAshotFactory extends AbstractAshotFactory @@ -37,9 +40,10 @@ public class WebAshotFactory extends AbstractAshotFactory scrollableElement) + { + return new ScrollbarHidingDecorator(strategy, scrollableElement, scrollbarHandler); + } + private AShot createAShot(String screenshotShootingStrategyName) { ScreenshotShootingStrategy configured = getStrategyBy(screenshotShootingStrategyName); ShootingStrategy baseShootingStrategy = getBaseShootingStrategy(); ShootingStrategy shootingStrategy = configured.getDecoratedShootingStrategy(baseShootingStrategy); - return new ScrollbarHidingAshot(Optional.empty(), scrollbarHandler).shootingStrategy(shootingStrategy) - .coordsProvider(configured instanceof SimpleScreenshotShootingStrategy - ? CeilingJsCoordsProvider.getSimple(javascriptActions) - : CeilingJsCoordsProvider.getScrollAdjusted(javascriptActions)); + shootingStrategy = decorateWithScrollbarHiding(shootingStrategy, Optional.empty()); + CoordsProvider coordsProvider = configured instanceof SimpleScreenshotShootingStrategy + ? CeilingJsCoordsProvider.getSimple(javascriptActions) + : CeilingJsCoordsProvider.getScrollAdjusted(javascriptActions); + return new AShot().shootingStrategy(shootingStrategy) + .coordsProvider(new ScrollBarHidingCoordsProviderDecorator(coordsProvider, scrollbarHandler)); } @Override diff --git a/vividus-plugin-web-app/src/main/java/org/vividus/selenium/screenshot/WebAdjustingCoordsProvider.java b/vividus-plugin-web-app/src/main/java/org/vividus/selenium/screenshot/WebCoordsProvider.java similarity index 79% rename from vividus-plugin-web-app/src/main/java/org/vividus/selenium/screenshot/WebAdjustingCoordsProvider.java rename to vividus-plugin-web-app/src/main/java/org/vividus/selenium/screenshot/WebCoordsProvider.java index 203a885fe9..fbff7585f3 100644 --- a/vividus-plugin-web-app/src/main/java/org/vividus/selenium/screenshot/WebAdjustingCoordsProvider.java +++ b/vividus-plugin-web-app/src/main/java/org/vividus/selenium/screenshot/WebCoordsProvider.java @@ -20,22 +20,19 @@ import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.vividus.selenium.manager.IWebDriverManager; -import org.vividus.ui.context.IUiContext; import ru.yandex.qatools.ashot.coordinates.Coords; +import ru.yandex.qatools.ashot.coordinates.WebDriverCoordsProvider; -public class WebAdjustingCoordsProvider extends AbstractAdjustingCoordsProvider +public class WebCoordsProvider extends WebDriverCoordsProvider { private static final long serialVersionUID = 3963826455192835938L; private final transient IWebDriverManager webDriverManager; - private final transient IScrollbarHandler scrollbarHandler; - public WebAdjustingCoordsProvider(IWebDriverManager webDriverManager, IScrollbarHandler scrollbarHandler, - IUiContext uiContext) + public WebCoordsProvider(IWebDriverManager webDriverManager, IScrollbarHandler scrollbarHandler) { - super(uiContext); this.webDriverManager = webDriverManager; this.scrollbarHandler = scrollbarHandler; } @@ -50,16 +47,10 @@ public Coords ofElement(WebDriver driver, WebElement element) { coords.y += Math.toIntExact(getYOffset(driver)); } - return adjustToSearchContext(coords); + return coords; }); } - @Override - protected Coords getCoords(WebElement webElement) - { - return super.ofElement(null, webElement); - } - private long getYOffset(WebDriver webDriver) { return executeScript(webDriver, "return pageYOffset"); diff --git a/vividus-plugin-web-app/src/main/java/org/vividus/selenium/screenshot/WebScreenshotCropper.java b/vividus-plugin-web-app/src/main/java/org/vividus/selenium/screenshot/WebScreenshotCropper.java new file mode 100644 index 0000000000..1a86677d60 --- /dev/null +++ b/vividus-plugin-web-app/src/main/java/org/vividus/selenium/screenshot/WebScreenshotCropper.java @@ -0,0 +1,62 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.vividus.selenium.screenshot; + +import java.awt.Point; +import java.util.Optional; + +import org.openqa.selenium.WebElement; +import org.vividus.selenium.IWebDriverProvider; +import org.vividus.ui.action.ISearchActions; +import org.vividus.ui.context.IUiContext; + +import ru.yandex.qatools.ashot.coordinates.Coords; +import ru.yandex.qatools.ashot.coordinates.CoordsProvider; + +public class WebScreenshotCropper extends ScreenshotCropper +{ + private final IUiContext uiContext; + + public WebScreenshotCropper(ScreenshotDebugger screenshotDebugger, ISearchActions searchActions, + CoordsProvider coordsProvider, IWebDriverProvider webDriverProvider, IUiContext uiContext) + { + super(screenshotDebugger, searchActions, coordsProvider, webDriverProvider); + this.uiContext = uiContext; + } + + @Override + protected Point calculateAdjustment(Optional contextCoords, int topAdjustment) + { + if (contextCoords.isEmpty()) + { + return super.calculateAdjustment(contextCoords, topAdjustment); + } + + /** + * This shift in x and y coords should be removed after the following issue is resolved: + * https://github.com/vividus-framework/vividus/issues/2883 + */ + WebElement context = uiContext.getSearchContext(WebElement.class).get(); + Coords currentContextCoords = getCoordsProvider().ofElement(getWebDriverProvider().get(), context); + Coords targetContextCoords = contextCoords.get(); + + int yShift = targetContextCoords.y - currentContextCoords.y; + int xShift = targetContextCoords.x - currentContextCoords.x; + + return new Point(xShift, yShift - topAdjustment); + } +} diff --git a/vividus-plugin-web-app/src/main/java/org/vividus/ui/web/screenshot/WebScreenshotParametersFactory.java b/vividus-plugin-web-app/src/main/java/org/vividus/ui/web/screenshot/WebScreenshotParametersFactory.java index c0c56e2324..7361ba8755 100644 --- a/vividus-plugin-web-app/src/main/java/org/vividus/ui/web/screenshot/WebScreenshotParametersFactory.java +++ b/vividus-plugin-web-app/src/main/java/org/vividus/ui/web/screenshot/WebScreenshotParametersFactory.java @@ -16,15 +16,20 @@ package org.vividus.ui.web.screenshot; +import java.util.Map; import java.util.Optional; +import java.util.Set; import org.openqa.selenium.WebElement; +import org.vividus.selenium.screenshot.IgnoreStrategy; import org.vividus.ui.action.ISearchActions; +import org.vividus.ui.action.search.Locator; import org.vividus.ui.screenshot.AbstractScreenshotParametersFactory; import org.vividus.ui.screenshot.ScreenshotParameters; import org.vividus.ui.screenshot.ScreenshotPrecondtionMismatchException; -public class WebScreenshotParametersFactory extends AbstractScreenshotParametersFactory +public class WebScreenshotParametersFactory + extends AbstractScreenshotParametersFactory { private final ISearchActions searchActions; @@ -36,29 +41,43 @@ public WebScreenshotParametersFactory(ISearchActions searchActions) @Override public Optional create(Optional screenshotConfiguration) { - return getScreenshotConfiguration(screenshotConfiguration, (c, b) -> c).map(config -> - { - WebScreenshotParameters parameters = createWithBaseConfiguration(config, - WebScreenshotParameters::new); - parameters.setNativeHeaderToCut(ensureValidCutSize(config.getNativeHeaderToCut(), "native header")); + return getScreenshotConfiguration(screenshotConfiguration, (c, b) -> c).map( + cfg -> configure(cfg, createWithBaseConfiguration(cfg))); + } - WebCutOptions webCutOptions = new WebCutOptions( - ensureValidCutSize(config.getWebHeaderToCut(), "web header"), - ensureValidCutSize(config.getWebFooterToCut(), "web footer") - ); - parameters.setWebCutOptions(webCutOptions); + @Override + public Optional create(Map> ignores) + { + WebScreenshotConfiguration configuration = getDefaultConfiguration().orElseGet(WebScreenshotConfiguration::new); + return Optional.of(configure(configuration, createWithBaseConfiguration(configuration, ignores))); + } - config.getScrollableElement().ifPresent(locator -> - { - WebElement scrollableElement = searchActions.findElement(locator).orElseThrow( - () -> new ScreenshotPrecondtionMismatchException("Scrollable element does not exist")); - parameters.setScrollableElement(Optional.of(scrollableElement)); - }); + @Override + protected WebScreenshotParameters createScreenshotParameters() + { + return new WebScreenshotParameters(); + } + + private ScreenshotParameters configure(WebScreenshotConfiguration config, WebScreenshotParameters parameters) + { + parameters.setNativeHeaderToCut(ensureValidCutSize(config.getNativeHeaderToCut(), "native header")); - parameters.setCoordsProvider(config.getCoordsProvider()); - parameters.setScrollTimeout(config.getScrollTimeout()); + WebCutOptions webCutOptions = new WebCutOptions( + ensureValidCutSize(config.getWebHeaderToCut(), "web header"), + ensureValidCutSize(config.getWebFooterToCut(), "web footer") + ); + parameters.setWebCutOptions(webCutOptions); - return parameters; + config.getScrollableElement().ifPresent(locator -> + { + WebElement scrollableElement = searchActions.findElement(locator).orElseThrow( + () -> new ScreenshotPrecondtionMismatchException("Scrollable element does not exist")); + parameters.setScrollableElement(Optional.of(scrollableElement)); }); + + parameters.setCoordsProvider(config.getCoordsProvider()); + parameters.setScrollTimeout(config.getScrollTimeout()); + + return parameters; } } diff --git a/vividus-plugin-web-app/src/main/java/org/vividus/selenium/screenshot/ScrollbarHidingAshot.java b/vividus-plugin-web-app/src/main/java/ru/yandex/qatools/ashot/shooting/ScrollbarHidingDecorator.java similarity index 51% rename from vividus-plugin-web-app/src/main/java/org/vividus/selenium/screenshot/ScrollbarHidingAshot.java rename to vividus-plugin-web-app/src/main/java/ru/yandex/qatools/ashot/shooting/ScrollbarHidingDecorator.java index 95b1a22b0c..7c810696b6 100644 --- a/vividus-plugin-web-app/src/main/java/org/vividus/selenium/screenshot/ScrollbarHidingAshot.java +++ b/vividus-plugin-web-app/src/main/java/ru/yandex/qatools/ashot/shooting/ScrollbarHidingDecorator.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 the original author or authors. + * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,46 +14,55 @@ * limitations under the License. */ -package org.vividus.selenium.screenshot; +package ru.yandex.qatools.ashot.shooting; +import java.awt.image.BufferedImage; import java.util.Optional; +import java.util.Set; import java.util.function.Supplier; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; +import org.vividus.selenium.screenshot.IScrollbarHandler; -import ru.yandex.qatools.ashot.AShot; -import ru.yandex.qatools.ashot.Screenshot; +import ru.yandex.qatools.ashot.coordinates.Coords; -class ScrollbarHidingAshot extends AShot +public class ScrollbarHidingDecorator extends ShootingDecorator { - private static final long serialVersionUID = -3976031178886339719L; + private static final long serialVersionUID = -6146195031592698438L; private final transient Optional scrollableElement; private final transient IScrollbarHandler scrollbarHandler; - ScrollbarHidingAshot(Optional scrollableElement, + public ScrollbarHidingDecorator(ShootingStrategy shootingStrategy, Optional scrollableElement, IScrollbarHandler scrollbarHandler) { + super(shootingStrategy); this.scrollableElement = scrollableElement; this.scrollbarHandler = scrollbarHandler; } @Override - public Screenshot takeScreenshot(WebDriver driver) + public BufferedImage getScreenshot(WebDriver wd) { - return takeWithHiddentScrollbars(() -> super.takeScreenshot(driver)); + return perform(() -> getShootingStrategy().getScreenshot(wd)); } @Override - public Screenshot takeScreenshot(WebDriver driver, WebElement webElement) + public BufferedImage getScreenshot(WebDriver wd, Set coords) { - return takeWithHiddentScrollbars(() -> super.takeScreenshot(driver, webElement)); + return perform(() -> getShootingStrategy().getScreenshot(wd, coords)); } - private Screenshot takeWithHiddentScrollbars(Supplier taker) + @Override + public Set prepareCoords(Set coordsSet) + { + return getShootingStrategy().prepareCoords(coordsSet); + } + + private BufferedImage perform(Supplier bufferedImageSupplier) { - return scrollableElement.map(e -> scrollbarHandler.performActionWithHiddenScrollbars(taker, e)) - .orElseGet(() -> scrollbarHandler.performActionWithHiddenScrollbars(taker)); + return scrollableElement.map(e -> scrollbarHandler.performActionWithHiddenScrollbars(bufferedImageSupplier, e)) + .orElseGet(() -> scrollbarHandler.performActionWithHiddenScrollbars(bufferedImageSupplier)); } } diff --git a/vividus-plugin-web-app/src/main/resources/spring.xml b/vividus-plugin-web-app/src/main/resources/spring.xml index ffc77f70f4..f9165a0333 100644 --- a/vividus-plugin-web-app/src/main/resources/spring.xml +++ b/vividus-plugin-web-app/src/main/resources/spring.xml @@ -9,7 +9,6 @@ http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd" default-lazy-init="true" profile="web"> - @@ -23,9 +22,9 @@ - + - + @@ -33,7 +32,10 @@ - + + + diff --git a/vividus-plugin-web-app/src/test/java/org/vividus/selenium/screenshot/ScrollBarHidingCoordsProviderDecoratorTests.java b/vividus-plugin-web-app/src/test/java/org/vividus/selenium/screenshot/ScrollBarHidingCoordsProviderDecoratorTests.java new file mode 100644 index 0000000000..25cf8e802c --- /dev/null +++ b/vividus-plugin-web-app/src/test/java/org/vividus/selenium/screenshot/ScrollBarHidingCoordsProviderDecoratorTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.vividus.selenium.screenshot; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.awt.image.BufferedImage; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; + +import ru.yandex.qatools.ashot.coordinates.Coords; +import ru.yandex.qatools.ashot.coordinates.CoordsProvider; + +@ExtendWith(MockitoExtension.class) +class ScrollBarHidingCoordsProviderDecoratorTests +{ + @Mock private CoordsProvider coordsProvider; + @Mock private ScrollbarHandler scrollbarHandler; + @Mock private Coords coords; + @Mock private WebDriver webDriver; + @Mock private WebElement webElement; + @Mock private BufferedImage bufferedImage; + + @Test + void shouldReturnCoordsOfElement() + { + when(scrollbarHandler.performActionWithHiddenScrollbars(argThat(s -> { + s.get(); + return true; + }))).thenReturn(coords); + ScrollBarHidingCoordsProviderDecorator decorator = new ScrollBarHidingCoordsProviderDecorator(coordsProvider, + scrollbarHandler); + Coords elementCoords = decorator.ofElement(webDriver, webElement); + assertEquals(coords, elementCoords); + verify(coordsProvider).ofElement(webDriver, webElement); + } +} diff --git a/vividus-plugin-web-app/src/test/java/org/vividus/selenium/screenshot/WebAshotFactoryTests.java b/vividus-plugin-web-app/src/test/java/org/vividus/selenium/screenshot/WebAshotFactoryTests.java index a1c489cffb..fed2555352 100644 --- a/vividus-plugin-web-app/src/test/java/org/vividus/selenium/screenshot/WebAshotFactoryTests.java +++ b/vividus-plugin-web-app/src/test/java/org/vividus/selenium/screenshot/WebAshotFactoryTests.java @@ -46,8 +46,11 @@ import org.vividus.ui.web.screenshot.WebScreenshotParameters; import ru.yandex.qatools.ashot.AShot; +import ru.yandex.qatools.ashot.coordinates.CoordsProvider; import ru.yandex.qatools.ashot.shooting.CuttingDecorator; +import ru.yandex.qatools.ashot.shooting.ElementCroppingDecorator; import ru.yandex.qatools.ashot.shooting.ScalingDecorator; +import ru.yandex.qatools.ashot.shooting.ScrollbarHidingDecorator; import ru.yandex.qatools.ashot.shooting.ShootingStrategy; import ru.yandex.qatools.ashot.shooting.cutter.CutStrategy; import ru.yandex.qatools.ashot.shooting.cutter.FixedCutStrategy; @@ -77,8 +80,10 @@ void shouldCreateAshotViaScreenshotShootingStrategyIfThereIsNoConfigurationFound webAshotFactory.setStrategies(Map.of(VIEWPORT_PASTING, new ViewportPastingScreenshotShootingStrategy())); webAshotFactory.setScreenshotShootingStrategy(VIEWPORT_PASTING); AShot aShot = webAshotFactory.create(Optional.empty()); - assertThat(FieldUtils.readField(aShot, COORDS_PROVIDER, true), is(instanceOf(CeilingJsCoordsProvider.class))); - assertThat(FieldUtils.readField(aShot, SHOOTING_STRATEGY, true), + validateCoordsProvider(aShot); + ShootingStrategy baseStrategy = (ShootingStrategy) FieldUtils.readField(aShot, SHOOTING_STRATEGY, true); + assertThat(baseStrategy, instanceOf(ScrollbarHidingDecorator.class)); + assertThat(FieldUtils.readField(baseStrategy, SHOOTING_STRATEGY, true), instanceOf(AdjustingViewportPastingDecorator.class)); } @@ -90,8 +95,10 @@ void shouldCreateAshotViaScreenshotShootingStrategyUsingStrategyFromConfiguratio WebScreenshotParameters screenshotParameters = new WebScreenshotParameters(); screenshotParameters.setShootingStrategy(Optional.of(VIEWPORT_PASTING)); AShot aShot = webAshotFactory.create(Optional.of(screenshotParameters)); - assertThat(FieldUtils.readField(aShot, COORDS_PROVIDER, true), is(instanceOf(CeilingJsCoordsProvider.class))); - assertThat(FieldUtils.readField(aShot, SHOOTING_STRATEGY, true), + validateCoordsProvider(aShot); + ShootingStrategy baseStrategy = (ShootingStrategy) FieldUtils.readField(aShot, SHOOTING_STRATEGY, true); + assertThat(baseStrategy, instanceOf(ScrollbarHidingDecorator.class)); + assertThat(FieldUtils.readField(baseStrategy, SHOOTING_STRATEGY, true), instanceOf(AdjustingViewportPastingDecorator.class)); } @@ -101,8 +108,11 @@ void shouldCreateAshotViaWithSimpleCoords() throws IllegalAccessException webAshotFactory.setStrategies(Map.of(SIMPLE, new SimpleScreenshotShootingStrategy())); webAshotFactory.setScreenshotShootingStrategy(SIMPLE); AShot aShot = webAshotFactory.create(Optional.empty()); - assertThat(FieldUtils.readField(aShot, COORDS_PROVIDER, true), is(instanceOf(CeilingJsCoordsProvider.class))); - assertThat(FieldUtils.readField(aShot, SHOOTING_STRATEGY, true), instanceOf(ViewportShootingStrategy.class)); + validateCoordsProvider(aShot); + ShootingStrategy baseStrategy = (ShootingStrategy) FieldUtils.readField(aShot, SHOOTING_STRATEGY, true); + assertThat(baseStrategy, instanceOf(ScrollbarHidingDecorator.class)); + assertThat(FieldUtils.readField(baseStrategy, SHOOTING_STRATEGY, true), + instanceOf(ViewportShootingStrategy.class)); } @Test @@ -119,9 +129,16 @@ void shouldCreateAshotWithCuttingStrategiesForNativeWebHeadersFooters() throws I AShot aShot = webAshotFactory.create(Optional.of(screenshotParameters)); - assertThat(FieldUtils.readField(aShot, COORDS_PROVIDER, true), is(instanceOf(CeilingJsCoordsProvider.class))); - ShootingStrategy viewportPastingDecorator = getShootingStrategy(aShot); - assertThat(viewportPastingDecorator, is(instanceOf(AdjustingViewportPastingDecorator.class))); + validateCoordsProvider(aShot); + ShootingStrategy baseStrategy = getShootingStrategy(aShot); + assertThat(baseStrategy, is(instanceOf(ElementCroppingDecorator.class))); + + ShootingStrategy scrollbarHidingDecorator = (ShootingStrategy) FieldUtils.readField(baseStrategy, + SHOOTING_STRATEGY, true); + assertThat(scrollbarHidingDecorator, is(instanceOf(ScrollbarHidingDecorator.class))); + + ShootingStrategy viewportPastingDecorator = (ShootingStrategy) FieldUtils.readField(scrollbarHidingDecorator, + SHOOTING_STRATEGY, true); assertEquals(500, (int) FieldUtils.readField(viewportPastingDecorator, "scrollTimeout", true)); assertEquals(screenshotDebugger, FieldUtils.readField(viewportPastingDecorator, "screenshotDebugger", true)); @@ -160,10 +177,19 @@ void shouldCreateAshotUsingScrollableElement() throws IllegalAccessException screenshotParameters.setScrollTimeout(Duration.ofMillis(TEN)); AShot aShot = webAshotFactory.create(Optional.of(screenshotParameters)); - assertThat(FieldUtils.readField(aShot, COORDS_PROVIDER, true), is(instanceOf(CeilingJsCoordsProvider.class))); - ShootingStrategy scrollableElementAwareDecorator = getShootingStrategy(aShot); + validateCoordsProvider(aShot); + ShootingStrategy decorator = getShootingStrategy(aShot); + assertThat(decorator, is(instanceOf(ElementCroppingDecorator.class))); + + ShootingStrategy scrollbarHidingDecorator = (ShootingStrategy) FieldUtils.readField(decorator, + SHOOTING_STRATEGY, true); + assertThat(scrollbarHidingDecorator, is(instanceOf(ScrollbarHidingDecorator.class))); + + ShootingStrategy scrollableElementAwareDecorator = (ShootingStrategy) FieldUtils + .readField(scrollbarHidingDecorator, SHOOTING_STRATEGY, true); assertThat(scrollableElementAwareDecorator, is(instanceOf(AdjustingScrollableElementAwareViewportPastingDecorator.class))); + assertEquals(webElement, FieldUtils.readField(scrollableElementAwareDecorator, "scrollableElement", true)); ShootingStrategy scalingDecorator = getShootingStrategy(scrollableElementAwareDecorator); @@ -171,6 +197,14 @@ void shouldCreateAshotUsingScrollableElement() throws IllegalAccessException verifyDPR(scalingDecorator); } + private void validateCoordsProvider(AShot aShot) throws IllegalAccessException + { + CoordsProvider coordsProvider = (CoordsProvider) FieldUtils.readField(aShot, COORDS_PROVIDER, true); + assertThat(coordsProvider, is(instanceOf(ScrollBarHidingCoordsProviderDecorator.class))); + coordsProvider = (CoordsProvider) FieldUtils.readField(coordsProvider, COORDS_PROVIDER, true); + assertThat(coordsProvider, is(instanceOf(CeilingJsCoordsProvider.class))); + } + private CutStrategy getCutStrategy(Object hasCutStrategy) throws IllegalAccessException { return (CutStrategy) FieldUtils.readField(hasCutStrategy, "cutStrategy", true); diff --git a/vividus-plugin-web-app/src/test/java/org/vividus/selenium/screenshot/WebAdjustingCoordsProviderTests.java b/vividus-plugin-web-app/src/test/java/org/vividus/selenium/screenshot/WebCoordsProviderTests.java similarity index 81% rename from vividus-plugin-web-app/src/test/java/org/vividus/selenium/screenshot/WebAdjustingCoordsProviderTests.java rename to vividus-plugin-web-app/src/test/java/org/vividus/selenium/screenshot/WebCoordsProviderTests.java index b14e2dd743..127a94b08f 100644 --- a/vividus-plugin-web-app/src/test/java/org/vividus/selenium/screenshot/WebAdjustingCoordsProviderTests.java +++ b/vividus-plugin-web-app/src/test/java/org/vividus/selenium/screenshot/WebCoordsProviderTests.java @@ -18,7 +18,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -34,12 +33,11 @@ import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.vividus.selenium.manager.IWebDriverManager; -import org.vividus.ui.context.IUiContext; import ru.yandex.qatools.ashot.coordinates.Coords; @ExtendWith(MockitoExtension.class) -class WebAdjustingCoordsProviderTests +class WebCoordsProviderTests { private static final int X = 1; private static final int Y = 2; @@ -56,14 +54,11 @@ class WebAdjustingCoordsProviderTests @Mock private IWebDriverManager webDriverManager; - @Mock - private IUiContext uiContext; - @Mock private IScrollbarHandler scrollbarHandler; @InjectMocks - private WebAdjustingCoordsProvider adjustingCoordsProvider; + private WebCoordsProvider adjustingCoordsProvider; @Test void testGetCoordsNotIOS() @@ -92,12 +87,8 @@ void testGetCoordsIOS() @Test void shouldIntersectIgnoredElementWithContextAndAdjustCoordinates() { - Coords expectedCoords = new Coords(0, 0, 3, 5); + Coords expectedCoords = new Coords(1, 2, 4, 6); mockScrollbarActions(expectedCoords); - WebElement searchContext = mock(WebElement.class); - when(searchContext.getLocation()).thenReturn(new Point(2, 3)); - when(searchContext.getSize()).thenReturn(new Dimension(5, 5)); - when(uiContext.getSearchContext()).thenReturn(searchContext); when(webElement.getLocation()).thenReturn(new Point(X, Y)); when(webElement.getSize()).thenReturn(new Dimension(4, 6)); Coords coords = adjustingCoordsProvider.ofElement(webDriver, webElement); @@ -108,12 +99,8 @@ void shouldIntersectIgnoredElementWithContextAndAdjustCoordinates() @Test void shouldRelateCoordsIfTheyInsideContext() { - Coords expectedCoords = new Coords(1, 1, WIDTH, HEIGHT); + Coords expectedCoords = new Coords(2, 2, WIDTH, HEIGHT); mockScrollbarActions(expectedCoords); - WebElement searchContext = mock(WebElement.class); - when(searchContext.getLocation()).thenReturn(new Point(1, 1)); - when(searchContext.getSize()).thenReturn(new Dimension(5, 5)); - when(uiContext.getSearchContext()).thenReturn(searchContext); when(webElement.getLocation()).thenReturn(new Point(2, 2)); when(webElement.getSize()).thenReturn(new Dimension(WIDTH, HEIGHT)); Coords coords = adjustingCoordsProvider.ofElement(webDriver, webElement); @@ -129,8 +116,8 @@ private void mockScrollbarActions() private void mockScrollbarActions(Coords expectedCoords) { - when(scrollbarHandler.performActionWithHiddenScrollbars( - argThat(a -> expectedCoords.equals(a.get())))).thenReturn(expectedCoords); + when(scrollbarHandler.performActionWithHiddenScrollbars(argThat(a -> expectedCoords.equals(a.get())))) + .thenReturn(expectedCoords); } private void verifyCoords(Coords coordsToCheck) diff --git a/vividus-plugin-web-app/src/test/java/org/vividus/selenium/screenshot/WebScreenshotCropperTests.java b/vividus-plugin-web-app/src/test/java/org/vividus/selenium/screenshot/WebScreenshotCropperTests.java new file mode 100644 index 0000000000..a207ed33a1 --- /dev/null +++ b/vividus-plugin-web-app/src/test/java/org/vividus/selenium/screenshot/WebScreenshotCropperTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.vividus.selenium.screenshot; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.image.BufferedImage; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.vividus.selenium.IWebDriverProvider; +import org.vividus.ui.action.ISearchActions; +import org.vividus.ui.action.search.Locator; +import org.vividus.ui.context.IUiContext; + +import ru.yandex.qatools.ashot.coordinates.Coords; +import ru.yandex.qatools.ashot.coordinates.CoordsProvider; + +@ExtendWith(MockitoExtension.class) +class WebScreenshotCropperTests +{ + @Mock private Locator elementLocator; + @Mock private WebDriver webDriver; + + @Mock private ISearchActions searchActions; + @Mock private CoordsProvider coordsProvider; + @Mock private IWebDriverProvider webDriverProvider; + @Mock private IUiContext uiContext; + @Mock private ScreenshotDebugger screenshotDebugger; + @InjectMocks private WebScreenshotCropper cropper; + + @Test + void shouldAdjustElementsToTargetContext() + { + when(webDriverProvider.get()).thenReturn(webDriver); + WebElement element = mock(WebElement.class); + when(searchActions.findElements(elementLocator)).thenReturn(List.of(element)); + when(coordsProvider.ofElements(webDriver, List.of(element))).thenReturn(Set.of(new Coords(100, 100, 100, 100))); + + WebElement currentContext = mock(WebElement.class); + when(uiContext.getSearchContext(WebElement.class)).thenReturn(Optional.of(currentContext)); + when(coordsProvider.ofElement(webDriver, currentContext)).thenReturn(new Coords(90, 90, 200, 200)); + + BufferedImage image = mock(BufferedImage.class); + Graphics2D g2 = mock(Graphics2D.class); + when(image.createGraphics()).thenReturn(g2); + + Coords targetContext = new Coords(95, 95, 200, 200); + + cropper.crop(image, Optional.of(targetContext), + Map.of(IgnoreStrategy.ELEMENT, Set.of(elementLocator)), 0); + + verify(g2).clearRect(105, 105, 100, 100); + } + + @Test + void shouldNotCalculateAdjustmentIfContextCoordsAreEmpty() + { + Point adjustment = cropper.calculateAdjustment(Optional.empty(), 42); + assertEquals(new Point(0, -42), adjustment); + verifyNoInteractions(webDriverProvider, coordsProvider); + } +} diff --git a/vividus-plugin-web-app/src/test/java/org/vividus/ui/web/screenshot/WebScreenshotParametersFactoryTests.java b/vividus-plugin-web-app/src/test/java/org/vividus/ui/web/screenshot/WebScreenshotParametersFactoryTests.java index 6db9a004b8..90ba737bae 100644 --- a/vividus-plugin-web-app/src/test/java/org/vividus/ui/web/screenshot/WebScreenshotParametersFactoryTests.java +++ b/vividus-plugin-web-app/src/test/java/org/vividus/ui/web/screenshot/WebScreenshotParametersFactoryTests.java @@ -19,11 +19,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.time.Duration; import java.util.Map; import java.util.Optional; +import java.util.Set; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -32,6 +34,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.openqa.selenium.WebElement; import org.vividus.selenium.screenshot.CoordsProviderType; +import org.vividus.selenium.screenshot.IgnoreStrategy; import org.vividus.ui.action.ISearchActions; import org.vividus.ui.action.search.Locator; import org.vividus.ui.screenshot.ScreenshotParameters; @@ -56,6 +59,7 @@ void shouldCreateScreenshotConfiguration() WebScreenshotConfiguration defaultConfiguration = new WebScreenshotConfiguration(); factory.setShootingStrategy(SIMPLE); factory.setScreenshotConfigurations(new PropertyMappedCollection<>(Map.of(SIMPLE, defaultConfiguration))); + factory.setIgnoreStrategies(Map.of()); WebScreenshotConfiguration userConfiguration = new WebScreenshotConfiguration(); userConfiguration.setShootingStrategy(Optional.of(SIMPLE)); @@ -83,11 +87,31 @@ void shouldCreateScreenshotConfiguration() assertEquals(Duration.ofSeconds(1), configuration.getScrollTimeout()); } + @Test + void shouldCreateScreenshotConfigurationWithIgnores() + { + WebScreenshotConfiguration defaultConfiguration = new WebScreenshotConfiguration(); + factory.setShootingStrategy(SIMPLE); + factory.setScreenshotConfigurations(new PropertyMappedCollection<>(Map.of(SIMPLE, defaultConfiguration))); + factory.setIgnoreStrategies(Map.of(IgnoreStrategy.ELEMENT, Set.of(), IgnoreStrategy.AREA, Set.of())); + + Locator locator = mock(Locator.class); + Map> ignores = Map.of( + IgnoreStrategy.ELEMENT, Set.of(locator), + IgnoreStrategy.AREA, Set.of(locator) + ); + Optional createdParameters = factory.create(ignores); + assertTrue(createdParameters.isPresent()); + WebScreenshotParameters configuration = (WebScreenshotParameters) createdParameters.get(); + assertEquals(ignores, configuration.getIgnoreStrategies()); + } + @Test void shouldFailIfElementByLocatorDoesNotExist() { factory.setShootingStrategy(SIMPLE); factory.setScreenshotConfigurations(new PropertyMappedCollection<>(Map.of())); + factory.setIgnoreStrategies(Map.of()); WebScreenshotConfiguration userParameters = new WebScreenshotConfiguration(); userParameters.setScrollableElement(Optional.of(locator)); diff --git a/vividus-plugin-web-app/src/test/java/org/vividus/selenium/screenshot/ScrollbarHidingAshotTests.java b/vividus-plugin-web-app/src/test/java/ru/yandex/qatools/ashot/shooting/ScrollbarHidingDecoratorTests.java similarity index 61% rename from vividus-plugin-web-app/src/test/java/org/vividus/selenium/screenshot/ScrollbarHidingAshotTests.java rename to vividus-plugin-web-app/src/test/java/ru/yandex/qatools/ashot/shooting/ScrollbarHidingDecoratorTests.java index 76bb987c9e..c0c31f1986 100644 --- a/vividus-plugin-web-app/src/test/java/org/vividus/selenium/screenshot/ScrollbarHidingAshotTests.java +++ b/vividus-plugin-web-app/src/test/java/ru/yandex/qatools/ashot/shooting/ScrollbarHidingDecoratorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 the original author or authors. + * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,12 +14,13 @@ * limitations under the License. */ -package org.vividus.selenium.screenshot; +package ru.yandex.qatools.ashot.shooting; import static org.junit.jupiter.api.Assertions.assertSame; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.awt.image.BufferedImage; @@ -32,56 +33,51 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; +import org.vividus.selenium.screenshot.ScrollbarHandler; import ru.yandex.qatools.ashot.coordinates.Coords; -import ru.yandex.qatools.ashot.coordinates.CoordsProvider; -import ru.yandex.qatools.ashot.shooting.ShootingStrategy; @ExtendWith(MockitoExtension.class) -class ScrollbarHidingAshotTests +class ScrollbarHidingDecoratorTests { @Mock private ShootingStrategy strategy; @Mock private ScrollbarHandler scrollbarHandler; @Mock private WebDriver webDriver; @Mock private BufferedImage bufferedImage; - @Mock private ru.yandex.qatools.ashot.Screenshot screenshot; @Test void shouldTakeScreenshotsWithHiddenScrollbars() { - ScrollbarHidingAshot ashot = new ScrollbarHidingAshot(Optional.empty(), scrollbarHandler); - ashot.shootingStrategy(strategy); + ScrollbarHidingDecorator decorator = new ScrollbarHidingDecorator(strategy, Optional.empty(), scrollbarHandler); when(scrollbarHandler.performActionWithHiddenScrollbars(argThat(s -> { s.get(); return true; - }))).thenReturn(screenshot); + }))).thenReturn(bufferedImage); when(strategy.getScreenshot(webDriver)).thenReturn(bufferedImage); - ashot.takeScreenshot(webDriver); - assertSame(screenshot, ashot.takeScreenshot(webDriver)); + assertSame(bufferedImage, decorator.getScreenshot(webDriver)); } @Test void shouldTakeScreenshotsWithElementAndWithHiddenScrollbars() { WebElement scrollableElement = mock(WebElement.class); - ScrollbarHidingAshot ashot = new ScrollbarHidingAshot(Optional.of(scrollableElement), scrollbarHandler); - ashot.shootingStrategy(strategy); - ashot.coordsProvider(new CoordsProvider() - { - private static final long serialVersionUID = 8784997908203644003L; - - @Override - public Coords ofElement(WebDriver driver, WebElement element) - { - return new Coords(0, 0, 0, 0); - } - }); + ScrollbarHidingDecorator decorator = new ScrollbarHidingDecorator(strategy, Optional.of(scrollableElement), + scrollbarHandler); when(scrollbarHandler.performActionWithHiddenScrollbars(argThat(s -> { s.get(); return true; - }), eq(scrollableElement))).thenReturn(screenshot); + }), eq(scrollableElement))).thenReturn(bufferedImage); when(strategy.getScreenshot(webDriver, Set.of())).thenReturn(bufferedImage); - ashot.takeScreenshot(webDriver, scrollableElement); - assertSame(screenshot, ashot.takeScreenshot(webDriver, scrollableElement)); + assertSame(bufferedImage, decorator.getScreenshot(webDriver, Set.of())); + } + + @Test + void shouldPrepareCoords() + { + ScrollbarHidingDecorator decorator = new ScrollbarHidingDecorator(strategy, Optional.empty(), + scrollbarHandler); + Coords coords = mock(Coords.class); + decorator.prepareCoords(Set.of(coords)); + verify(strategy).prepareCoords(Set.of(coords)); } } diff --git a/vividus-tests/src/main/resources/baselines/ipad-ignore-element.png b/vividus-tests/src/main/resources/baselines/ipad-ignore-element.png new file mode 100644 index 0000000000000000000000000000000000000000..87e23c9e925e485f58cf1f9a4a0790b2012716d0 GIT binary patch literal 19237 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