From f5f62789f7f16471362060ad9e5fa160fdd680c6 Mon Sep 17 00:00:00 2001 From: Aleksey2 Meleshko Date: Fri, 3 Mar 2023 18:00:00 +0100 Subject: [PATCH 01/12] Implement ImageComparator with tests, add draft of VisualStateProvider --- .../core/applications/AqualityModule.java | 6 +- .../core/visualization/IImageComparator.java | 27 +++++++ .../visualization/IVisualizationModule.java | 13 +++ .../core/visualization/ImageComparator.java | 71 ++++++++++++++++ .../visualization/VisualStateProvider.java | 43 ++++++++++ .../applications/browser/CachedLabel.java | 5 ++ .../visualization/ImageComparatorTests.java | 80 +++++++++++++++++++ 7 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 src/main/java/aquality/selenium/core/visualization/IImageComparator.java create mode 100644 src/main/java/aquality/selenium/core/visualization/IVisualizationModule.java create mode 100644 src/main/java/aquality/selenium/core/visualization/ImageComparator.java create mode 100644 src/main/java/aquality/selenium/core/visualization/VisualStateProvider.java create mode 100644 src/test/java/tests/visualization/ImageComparatorTests.java diff --git a/src/main/java/aquality/selenium/core/applications/AqualityModule.java b/src/main/java/aquality/selenium/core/applications/AqualityModule.java index 8411a45..989a9aa 100644 --- a/src/main/java/aquality/selenium/core/applications/AqualityModule.java +++ b/src/main/java/aquality/selenium/core/applications/AqualityModule.java @@ -12,6 +12,8 @@ import aquality.selenium.core.utilities.IElementActionRetrier; import aquality.selenium.core.utilities.ISettingsFile; import aquality.selenium.core.utilities.IUtilitiesModule; +import aquality.selenium.core.visualization.IImageComparator; +import aquality.selenium.core.visualization.IVisualizationModule; import aquality.selenium.core.waitings.IConditionalWait; import aquality.selenium.core.waitings.IWaitingsModule; import com.google.inject.AbstractModule; @@ -22,7 +24,8 @@ * Describes all dependencies which is registered for the project. */ public class AqualityModule extends AbstractModule - implements IConfigurationsModule, IElementsModule, ILocalizationModule, IUtilitiesModule, IWaitingsModule { + implements IConfigurationsModule, IElementsModule, ILocalizationModule, IUtilitiesModule, IWaitingsModule, + IVisualizationModule { private final Provider applicationProvider; @@ -49,5 +52,6 @@ protected void configure() { bind(IConditionalWait.class).to(getConditionalWaitImplementation()); bind(IElementFinder.class).to(getElementFinderImplementation()); bind(IElementFactory.class).to(getElementFactoryImplementation()); + bind(IImageComparator.class).to(getImageComparatorImplementation()); } } diff --git a/src/main/java/aquality/selenium/core/visualization/IImageComparator.java b/src/main/java/aquality/selenium/core/visualization/IImageComparator.java new file mode 100644 index 0000000..91a1f07 --- /dev/null +++ b/src/main/java/aquality/selenium/core/visualization/IImageComparator.java @@ -0,0 +1,27 @@ +package aquality.selenium.core.visualization; + +import java.awt.*; + +/** + * Compares images with defined threshold. + * Default implementation does resize and gray-scaling to simplify comparison. + */ +public interface IImageComparator { + + /** + * Gets the difference between two images as a percentage. + * @param thisOne The first image + * @param theOtherOne The image to compare with + * @param threshold How big a difference will be ignored as a percentage - value between 0 and 1. + * @return The difference between the two images as a percentage - value between 0 and 1. + */ + float percentageDifference(Image thisOne, Image theOtherOne, float threshold); + + /** + * Gets the difference between two images as a percentage. + * @param thisOne The first image + * @param theOtherOne The image to compare with + * @return The difference between the two images as a percentage - value between 0 and 1. + */ + float percentageDifference(Image thisOne, Image theOtherOne); +} diff --git a/src/main/java/aquality/selenium/core/visualization/IVisualizationModule.java b/src/main/java/aquality/selenium/core/visualization/IVisualizationModule.java new file mode 100644 index 0000000..6f0f681 --- /dev/null +++ b/src/main/java/aquality/selenium/core/visualization/IVisualizationModule.java @@ -0,0 +1,13 @@ +package aquality.selenium.core.visualization; + +/** + * Describes implementations of visualization services to be registered in DI container. + */ +public interface IVisualizationModule { + /** + * @return class which implements {@link IImageComparator} + */ + default Class getImageComparatorImplementation() { + return ImageComparator.class; + } +} diff --git a/src/main/java/aquality/selenium/core/visualization/ImageComparator.java b/src/main/java/aquality/selenium/core/visualization/ImageComparator.java new file mode 100644 index 0000000..151a1af --- /dev/null +++ b/src/main/java/aquality/selenium/core/visualization/ImageComparator.java @@ -0,0 +1,71 @@ +package aquality.selenium.core.visualization; + +import java.awt.*; +import java.awt.image.BufferedImage; + +public class ImageComparator implements IImageComparator { + private static final int DEFAULT_THRESHOLD = 3; + private static final int THRESHOLD_DIVISOR = 255; + private final int comparisonHeight = 16; + private final int comparisonWidth = 16; + + public float percentageDifference(Image thisOne, Image theOtherOne, float threshold) { + if (threshold < 0 || threshold > 1) { + throw new IllegalArgumentException(String.format("Threshold should be between 0 and 1, but was [%s]", threshold)); + } + + int intThreshold = Float.valueOf(threshold * THRESHOLD_DIVISOR).intValue(); + return percentageDifference(thisOne, theOtherOne, intThreshold); + } + + public float percentageDifference(Image thisOne, Image theOtherOne) { + return percentageDifference(thisOne, theOtherOne, DEFAULT_THRESHOLD); + } + + protected float percentageDifference(Image thisOne, Image theOtherOne, int threshold) { + int[][] differences = getDifferences(thisOne, theOtherOne); + + int diffPixels = 0; + + for (int[] bytes : differences) { + for (int b : bytes) { + if (b > threshold) { + diffPixels++; + } + } + } + + return diffPixels / (float) (comparisonWidth * comparisonHeight); + } + + protected int[][] getDifferences(Image thisOne, Image theOtherOne) { + int[][] firstGray = getResizedGrayScaleValues(thisOne); + int[][] secondGray = getResizedGrayScaleValues(theOtherOne); + + int[][] differences = new int[comparisonWidth][comparisonHeight]; + for (int y = 0; y < comparisonHeight; y++) { + for (int x = 0; x < comparisonWidth; x++) { + differences[x][y] = (byte) Math.abs(firstGray[x][y] - secondGray[x][y]); + } + } + + return differences; + } + + protected int[][] getResizedGrayScaleValues(Image image) { + BufferedImage resizedImage = new BufferedImage(comparisonWidth, comparisonHeight, BufferedImage.TYPE_BYTE_GRAY); + Graphics2D graphics2D = resizedImage.createGraphics(); + graphics2D.drawImage(image, 0, 0, comparisonWidth, comparisonHeight, null); + graphics2D.dispose(); + int[][] grayScale = new int[comparisonWidth][comparisonHeight]; + for (int y = 0; y < comparisonHeight; y++) { + for (int x = 0; x < comparisonWidth; x++) { + int pixel = resizedImage.getRGB(x, y); + int red = (pixel >> 16) & 0xff; + grayScale[x][y] = Math.abs(red); + } + } + + return grayScale; + } +} diff --git a/src/main/java/aquality/selenium/core/visualization/VisualStateProvider.java b/src/main/java/aquality/selenium/core/visualization/VisualStateProvider.java new file mode 100644 index 0000000..b5723f4 --- /dev/null +++ b/src/main/java/aquality/selenium/core/visualization/VisualStateProvider.java @@ -0,0 +1,43 @@ +package aquality.selenium.core.visualization; + +import org.openqa.selenium.Dimension; +import org.openqa.selenium.OutputType; +import org.openqa.selenium.Point; +import org.openqa.selenium.remote.RemoteWebElement; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.function.Supplier; + +import static java.awt.image.BufferedImage.TYPE_INT_RGB; + +public class VisualStateProvider { + + private final Supplier getElement; + + public VisualStateProvider(Supplier getElement){ + this.getElement = getElement; + } + + public Dimension getSize() { + return getElement.get().getSize(); + } + + public Point getLocation() { + return getElement.get().getLocation(); + } + + public Image getImage() { + byte[] bytes = getElement.get().getScreenshotAs(OutputType.BYTES); + try (InputStream is = new ByteArrayInputStream(bytes)) { + return ImageIO.read(is); + } catch (IOException exception) { + //log + return new BufferedImage(0, 0, TYPE_INT_RGB); + } + } +} diff --git a/src/test/java/tests/applications/browser/CachedLabel.java b/src/test/java/tests/applications/browser/CachedLabel.java index 55531bd..bfad7b4 100644 --- a/src/test/java/tests/applications/browser/CachedLabel.java +++ b/src/test/java/tests/applications/browser/CachedLabel.java @@ -5,6 +5,7 @@ import aquality.selenium.core.elements.interfaces.IElementFinder; import aquality.selenium.core.localization.ILocalizationManager; import aquality.selenium.core.localization.ILocalizedLogger; +import aquality.selenium.core.visualization.VisualStateProvider; import aquality.selenium.core.waitings.IConditionalWait; import org.openqa.selenium.By; import tests.applications.ICachedElement; @@ -58,4 +59,8 @@ public ILocalizedLogger getLocalizedLogger() { public ILocalizationManager getLocalizationManager() { return AqualityServices.get(ILocalizationManager.class); } + + public VisualStateProvider visual() { + return new VisualStateProvider(this::getElement); + } } diff --git a/src/test/java/tests/visualization/ImageComparatorTests.java b/src/test/java/tests/visualization/ImageComparatorTests.java new file mode 100644 index 0000000..7f5982b --- /dev/null +++ b/src/test/java/tests/visualization/ImageComparatorTests.java @@ -0,0 +1,80 @@ +package tests.visualization; + +import aquality.selenium.core.visualization.IImageComparator; +import org.testng.Assert; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import tests.applications.browser.AqualityServices; +import tests.applications.browser.ITheInternetPageTest; +import theinternet.DynamicLoadingForm; +import theinternet.TheInternetPage; + +import java.awt.*; + +public class ImageComparatorTests implements ITheInternetPageTest { + private final IImageComparator imageComparator = AqualityServices.get(IImageComparator.class); + + private void startLoading() { + DynamicLoadingForm.getStartLabel().click(); + } + + @Override + @BeforeMethod + public void beforeMethod() { + navigate(TheInternetPage.DYNAMIC_LOADING); + } + + @Test + public void testGetPercentageDifferenceForSameElement() { + Image firstImage = DynamicLoadingForm.getStartLabel().visual().getImage(); + Image secondImage = DynamicLoadingForm.getStartLabel().visual().getImage(); + + Assert.assertEquals(imageComparator.percentageDifference(firstImage, secondImage), 0); + } + + @Test + public void testGetPercentageDifferenceForSameElementWithZeroThreshold() { + Image firstImage = DynamicLoadingForm.getStartLabel().visual().getImage(); + Image secondImage = DynamicLoadingForm.getStartLabel().visual().getImage(); + + Assert.assertEquals(imageComparator.percentageDifference(firstImage, secondImage, 0), 0); + } + + @Test + public void testGetPercentageDifferenceForDifferentElements() { + Image firstImage = DynamicLoadingForm.getStartLabel().visual().getImage(); + startLoading(); + Image secondImage = DynamicLoadingForm.getLoadingLabel().visual().getImage(); + + Assert.assertNotEquals(imageComparator.percentageDifference(firstImage, secondImage), 0); + } + + @Test + public void testGetPercentageDifferenceForDifferentElementsWithFullThreshold() { + final int threshold = 1; + Image firstImage = DynamicLoadingForm.getStartLabel().visual().getImage(); + startLoading(); + Image secondImage = DynamicLoadingForm.getLoadingLabel().visual().getImage(); + + Assert.assertEquals(imageComparator.percentageDifference(firstImage, secondImage, threshold), 0); + } + + @Test + public void testGetPercentageDifferenceForSimilarElements() throws InterruptedException { + startLoading(); + Image firstImage = DynamicLoadingForm.getLoadingLabel().visual().getImage(); + Thread.sleep(300); + Image secondImage = DynamicLoadingForm.getLoadingLabel().visual().getImage(); + + Assert.assertTrue(imageComparator.percentageDifference(firstImage, secondImage, 0) != 0, + "With zero threshold, there should be some difference"); + Assert.assertTrue(imageComparator.percentageDifference(firstImage, secondImage, 0.2f) <= 0.3, + "With 0.2f threshold, the difference should be less or equal than 0.3"); + Assert.assertTrue(imageComparator.percentageDifference(firstImage, secondImage, 0.4f) <= 0.2, + "With 0.4f threshold, the difference should be less or equal than 0.2"); + Assert.assertTrue(imageComparator.percentageDifference(firstImage, secondImage, 0.6f) <= 0.1, + "With 0.6f threshold, the difference should be less or equal than 0.1"); + Assert.assertEquals(imageComparator.percentageDifference(firstImage, secondImage, 0.6f), 0, + "With 0.8f threshold, the difference should be 0"); + } +} From e1039a4212dba72661907415c7734ce25d554e31 Mon Sep 17 00:00:00 2001 From: Aleksey2 Meleshko Date: Tue, 7 Mar 2023 17:30:11 +0100 Subject: [PATCH 02/12] Implement VisualStateProvider and tests for it Add draft for VisualConfiguration and settings for it, add it to DI module Add localization values for logger moved ILogElementState to logging package --- .../core/applications/AqualityModule.java | 1 + .../configurations/IConfigurationsModule.java | 7 ++ .../configurations/IVisualConfiguration.java | 12 +++ .../configurations/VisualConfiguration.java | 40 ++++++++ .../elements/CachedElementStateProvider.java | 2 +- .../elements/DefaultElementStateProvider.java | 2 +- .../selenium/core/elements/Element.java | 16 ++++ .../core/elements/ElementStateProvider.java | 2 +- .../core/elements/interfaces/IElement.java | 7 ++ .../ILogElementState.java | 2 +- .../core/logging/ILogVisualState.java | 13 +++ .../visualization/IVisualStateProvider.java | 45 +++++++++ .../core/visualization/ImageFunctions.java | 61 +++++++++++++ .../visualization/VisualStateProvider.java | 78 ++++++++++++---- src/main/resources/localization/core.be.json | 22 ++++- src/main/resources/localization/core.en.json | 20 +++- src/main/resources/localization/core.pl.json | 22 ++++- src/main/resources/localization/core.ru.json | 22 ++++- src/main/resources/localization/core.uk.json | 20 +++- src/main/resources/settings.json | 8 ++ .../tests/applications/ICachedElement.java | 2 +- .../applications/browser/CachedLabel.java | 6 +- .../browser/ChromeApplication.java | 5 +- .../tests/elements/factory/CustomElement.java | 6 ++ .../elements/factory/CustomWebElement.java | 6 ++ .../visualization/ImageComparatorTests.java | 29 ++++-- .../VisualStateProviderTests.java | 91 +++++++++++++++++++ 27 files changed, 500 insertions(+), 47 deletions(-) create mode 100644 src/main/java/aquality/selenium/core/configurations/IVisualConfiguration.java create mode 100644 src/main/java/aquality/selenium/core/configurations/VisualConfiguration.java rename src/main/java/aquality/selenium/core/{elements/interfaces => logging}/ILogElementState.java (85%) create mode 100644 src/main/java/aquality/selenium/core/logging/ILogVisualState.java create mode 100644 src/main/java/aquality/selenium/core/visualization/IVisualStateProvider.java create mode 100644 src/main/java/aquality/selenium/core/visualization/ImageFunctions.java create mode 100644 src/test/java/tests/visualization/VisualStateProviderTests.java diff --git a/src/main/java/aquality/selenium/core/applications/AqualityModule.java b/src/main/java/aquality/selenium/core/applications/AqualityModule.java index 989a9aa..f547c38 100644 --- a/src/main/java/aquality/selenium/core/applications/AqualityModule.java +++ b/src/main/java/aquality/selenium/core/applications/AqualityModule.java @@ -45,6 +45,7 @@ protected void configure() { bind(ITimeoutConfiguration.class).to(getTimeoutConfigurationImplementation()).in(Singleton.class); bind(IRetryConfiguration.class).to(getRetryConfigurationImplementation()).in(Singleton.class); bind(IElementCacheConfiguration.class).to(getElementCacheConfigurationImplementation()).in(Singleton.class); + bind(IVisualConfiguration.class).to(getVisualConfigurationImplementation()).in(Singleton.class); bind(IElementActionRetrier.class).to(getElementActionRetrierImplementation()).in(Singleton.class); bind(IActionRetrier.class).to(getActionRetrierImplementation()).in(Singleton.class); bind(ILocalizationManager.class).to(getLocalizationManagerImplementation()).in(Singleton.class); diff --git a/src/main/java/aquality/selenium/core/configurations/IConfigurationsModule.java b/src/main/java/aquality/selenium/core/configurations/IConfigurationsModule.java index 980e728..0fddf83 100644 --- a/src/main/java/aquality/selenium/core/configurations/IConfigurationsModule.java +++ b/src/main/java/aquality/selenium/core/configurations/IConfigurationsModule.java @@ -4,6 +4,13 @@ * Describes implementations of configurations to be registered in DI container. */ public interface IConfigurationsModule { + /** + * @return class which implements {@link IVisualConfiguration} + */ + default Class getVisualConfigurationImplementation() { + return VisualConfiguration.class; + } + /** * @return class which implements {@link IElementCacheConfiguration} */ diff --git a/src/main/java/aquality/selenium/core/configurations/IVisualConfiguration.java b/src/main/java/aquality/selenium/core/configurations/IVisualConfiguration.java new file mode 100644 index 0000000..d062bfb --- /dev/null +++ b/src/main/java/aquality/selenium/core/configurations/IVisualConfiguration.java @@ -0,0 +1,12 @@ +package aquality.selenium.core.configurations; + +/** + * Represents visualization configuration, used for image comparison. + */ +public interface IVisualConfiguration { + /** + * Image format for comparison. + * @return image format. + */ + String getImageFormat(); +} diff --git a/src/main/java/aquality/selenium/core/configurations/VisualConfiguration.java b/src/main/java/aquality/selenium/core/configurations/VisualConfiguration.java new file mode 100644 index 0000000..5ef1323 --- /dev/null +++ b/src/main/java/aquality/selenium/core/configurations/VisualConfiguration.java @@ -0,0 +1,40 @@ +package aquality.selenium.core.configurations; + +import aquality.selenium.core.utilities.ISettingsFile; +import com.google.inject.Inject; + +import javax.imageio.ImageIO; +import java.util.Arrays; + +/** + * Represents visualization configuration, used for image comparison. + * Uses {@link ISettingsFile} as source for configuration values. + */ +public class VisualConfiguration implements IVisualConfiguration { + private String imageFormat; + private final ISettingsFile settingsFile; + + /** + * Instantiates class using {@link ISettingsFile} with visualization settings. + * @param settingsFile settings file. + */ + @Inject + public VisualConfiguration(ISettingsFile settingsFile) { + this.settingsFile = settingsFile; + } + + @Override + public String getImageFormat() { + if (imageFormat == null) { + String[] supportedFormats = ImageIO.getWriterFormatNames(); + String valueFromConfig = settingsFile.getValueOrDefault("/visualization/imageExtension", "png").toString(); + String actualFormat = valueFromConfig.startsWith(".") ? valueFromConfig.substring(1) : valueFromConfig; + if (Arrays.stream(supportedFormats).noneMatch(format -> format.equals(actualFormat))) { + throw new IllegalArgumentException(String.format( + "Format [%s] is not supported by current JRE. Supported formats: %s", actualFormat, Arrays.toString(supportedFormats))); + } + imageFormat = actualFormat; + } + return imageFormat; + } +} diff --git a/src/main/java/aquality/selenium/core/elements/CachedElementStateProvider.java b/src/main/java/aquality/selenium/core/elements/CachedElementStateProvider.java index e29d034..331d6e7 100644 --- a/src/main/java/aquality/selenium/core/elements/CachedElementStateProvider.java +++ b/src/main/java/aquality/selenium/core/elements/CachedElementStateProvider.java @@ -1,7 +1,7 @@ package aquality.selenium.core.elements; import aquality.selenium.core.elements.interfaces.IElementCacheHandler; -import aquality.selenium.core.elements.interfaces.ILogElementState; +import aquality.selenium.core.logging.ILogElementState; import aquality.selenium.core.waitings.IConditionalWait; import org.openqa.selenium.By; import org.openqa.selenium.NoSuchElementException; diff --git a/src/main/java/aquality/selenium/core/elements/DefaultElementStateProvider.java b/src/main/java/aquality/selenium/core/elements/DefaultElementStateProvider.java index 0da4522..7ce9d16 100644 --- a/src/main/java/aquality/selenium/core/elements/DefaultElementStateProvider.java +++ b/src/main/java/aquality/selenium/core/elements/DefaultElementStateProvider.java @@ -1,7 +1,7 @@ package aquality.selenium.core.elements; import aquality.selenium.core.elements.interfaces.IElementFinder; -import aquality.selenium.core.elements.interfaces.ILogElementState; +import aquality.selenium.core.logging.ILogElementState; import aquality.selenium.core.waitings.IConditionalWait; import org.openqa.selenium.By; diff --git a/src/main/java/aquality/selenium/core/elements/Element.java b/src/main/java/aquality/selenium/core/elements/Element.java index 390bc93..63e24c4 100644 --- a/src/main/java/aquality/selenium/core/elements/Element.java +++ b/src/main/java/aquality/selenium/core/elements/Element.java @@ -6,8 +6,13 @@ import aquality.selenium.core.elements.interfaces.*; import aquality.selenium.core.localization.ILocalizationManager; import aquality.selenium.core.localization.ILocalizedLogger; +import aquality.selenium.core.logging.ILogElementState; +import aquality.selenium.core.logging.ILogVisualState; import aquality.selenium.core.logging.Logger; import aquality.selenium.core.utilities.IElementActionRetrier; +import aquality.selenium.core.visualization.IImageComparator; +import aquality.selenium.core.visualization.IVisualStateProvider; +import aquality.selenium.core.visualization.VisualStateProvider; import aquality.selenium.core.waitings.IConditionalWait; import org.openqa.selenium.By; import org.openqa.selenium.NoSuchElementException; @@ -37,6 +42,8 @@ protected Element(final By loc, final String name, final ElementState state) { protected abstract IElementFinder getElementFinder(); + protected abstract IImageComparator getImageComparator(); + protected abstract IElementCacheConfiguration getElementCacheConfiguration(); protected abstract IElementActionRetrier getElementActionRetrier(); @@ -70,6 +77,10 @@ protected ILogElementState logElementState() { getLocalizationManager().getLocalizedMessage(stateKey))); } + protected ILogVisualState logVisualState() { + return this::logElementAction; + } + @Override public By getLocator() { return locator; @@ -87,6 +98,11 @@ public IElementStateProvider state() { : new DefaultElementStateProvider(locator, getConditionalWait(), getElementFinder(), logElementState()); } + @Override + public IVisualStateProvider visual() { + return new VisualStateProvider(getImageComparator(), getElementActionRetrier(), this::getElement, logVisualState()); + } + @Override public RemoteWebElement getElement(Duration timeout) { try { diff --git a/src/main/java/aquality/selenium/core/elements/ElementStateProvider.java b/src/main/java/aquality/selenium/core/elements/ElementStateProvider.java index 255db0a..1e16e4f 100644 --- a/src/main/java/aquality/selenium/core/elements/ElementStateProvider.java +++ b/src/main/java/aquality/selenium/core/elements/ElementStateProvider.java @@ -1,7 +1,7 @@ package aquality.selenium.core.elements; import aquality.selenium.core.elements.interfaces.IElementStateProvider; -import aquality.selenium.core.elements.interfaces.ILogElementState; +import aquality.selenium.core.logging.ILogElementState; import org.openqa.selenium.WebElement; public abstract class ElementStateProvider implements IElementStateProvider { diff --git a/src/main/java/aquality/selenium/core/elements/interfaces/IElement.java b/src/main/java/aquality/selenium/core/elements/interfaces/IElement.java index a7871f5..ddf509a 100644 --- a/src/main/java/aquality/selenium/core/elements/interfaces/IElement.java +++ b/src/main/java/aquality/selenium/core/elements/interfaces/IElement.java @@ -1,5 +1,6 @@ package aquality.selenium.core.elements.interfaces; +import aquality.selenium.core.visualization.IVisualStateProvider; import org.openqa.selenium.By; import org.openqa.selenium.remote.RemoteWebElement; @@ -27,6 +28,12 @@ public interface IElement extends IParent { */ IElementStateProvider state(); + /** + * Gets element visual state. + * @return provider to define element's visual state. + */ + IVisualStateProvider visual(); + /** * Gets current element by specified {@link #getLocator()} * Default timeout is provided in {@link aquality.selenium.core.configurations.ITimeoutConfiguration} diff --git a/src/main/java/aquality/selenium/core/elements/interfaces/ILogElementState.java b/src/main/java/aquality/selenium/core/logging/ILogElementState.java similarity index 85% rename from src/main/java/aquality/selenium/core/elements/interfaces/ILogElementState.java rename to src/main/java/aquality/selenium/core/logging/ILogElementState.java index bb388e4..d6275e8 100644 --- a/src/main/java/aquality/selenium/core/elements/interfaces/ILogElementState.java +++ b/src/main/java/aquality/selenium/core/logging/ILogElementState.java @@ -1,4 +1,4 @@ -package aquality.selenium.core.elements.interfaces; +package aquality.selenium.core.logging; /** * Describes interface that can log element state. diff --git a/src/main/java/aquality/selenium/core/logging/ILogVisualState.java b/src/main/java/aquality/selenium/core/logging/ILogVisualState.java new file mode 100644 index 0000000..6ade082 --- /dev/null +++ b/src/main/java/aquality/selenium/core/logging/ILogVisualState.java @@ -0,0 +1,13 @@ +package aquality.selenium.core.logging; + +/** + * Describes interface that can log visual state. + */ +public interface ILogVisualState { + /** + * Logs element visual state. + * @param messageKey key of localized message to log. + * @param args values to put into localized message (if any). + */ + void logVisualState(String messageKey, Object... args); +} diff --git a/src/main/java/aquality/selenium/core/visualization/IVisualStateProvider.java b/src/main/java/aquality/selenium/core/visualization/IVisualStateProvider.java new file mode 100644 index 0000000..7fb2b82 --- /dev/null +++ b/src/main/java/aquality/selenium/core/visualization/IVisualStateProvider.java @@ -0,0 +1,45 @@ +package aquality.selenium.core.visualization; + +import java.awt.*; + +/** + * Provides visual state of the element. + */ +public interface IVisualStateProvider { + + /** + * Gets a size object containing the height and width of this element. + * @return size of the element. + */ + Dimension getSize(); + + /** + * Gets a point object containing the coordinates of the upper-left + * corner of this element relative to the upper-left corner of the page. + * @return location of the element. + */ + Point getLocation(); + + /** + * Gets an image containing the screenshot of the element. + * @return screenshot of the element. + */ + Image getImage(); + + /** + * Gets the difference between the image of the element and the provided image using {@link IImageComparator}. + * @param theOtherOne the image to compare the element with. + * @param threshold how big a difference will be ignored as a percentage - value between 0 and 1. + * If the value is null, the default value is got from {@link aquality.selenium.core.configurations.IVisualConfiguration}. + * @return the difference between the two images as a percentage - value between 0 and 1. + */ + float getDifference(Image theOtherOne, float threshold); + + /** + * Gets the difference between the image of the element and the provided image using {@link IImageComparator}. + * The threshold value is got from {@link aquality.selenium.core.configurations.IVisualConfiguration}. + * @param theOtherOne the image to compare the element with. + * @return the difference between the two images as a percentage - value between 0 and 1. + */ + float getDifference(Image theOtherOne); +} diff --git a/src/main/java/aquality/selenium/core/visualization/ImageFunctions.java b/src/main/java/aquality/selenium/core/visualization/ImageFunctions.java new file mode 100644 index 0000000..c1fef52 --- /dev/null +++ b/src/main/java/aquality/selenium/core/visualization/ImageFunctions.java @@ -0,0 +1,61 @@ +package aquality.selenium.core.visualization; + +import aquality.selenium.core.elements.interfaces.IElement; +import aquality.selenium.core.logging.Logger; +import org.openqa.selenium.OutputType; +import org.openqa.selenium.remote.RemoteWebElement; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.RenderedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import static java.awt.image.BufferedImage.TYPE_INT_RGB; + +/** + * Image and screenshot extensions. + */ +public class ImageFunctions { + /** + * Represents given element's screenshot as an image. + * @param element given element. + * @return image object. + */ + public static BufferedImage getScreenshotAsImage(IElement element) { + return getScreenshotAsImage(element.getElement()); + } + + /** + * Represents given element's screenshot as an image. + * @param element given element. + * @return image object. + */ + public static BufferedImage getScreenshotAsImage(RemoteWebElement element) { + byte[] bytes = element.getScreenshotAs(OutputType.BYTES); + try (InputStream is = new ByteArrayInputStream(bytes)) { + return ImageIO.read(is); + } catch (IOException exception) { + Logger.getInstance().fatal("Failed to get element's screenshot as an image", exception); + return new BufferedImage(0, 0, TYPE_INT_RGB); + } + } + + /** + * Represents dimension of the given image. + * @param image given image. + * @return size of the given image. + */ + public static Dimension getSize(Image image) { + if (image instanceof RenderedImage) { + RenderedImage renderedImage = (RenderedImage) image; + return new Dimension(renderedImage.getWidth(), renderedImage.getHeight()); + } + else { + return new Dimension(image.getWidth(null), image.getHeight(null)); + } + + } +} diff --git a/src/main/java/aquality/selenium/core/visualization/VisualStateProvider.java b/src/main/java/aquality/selenium/core/visualization/VisualStateProvider.java index b5723f4..1f0a055 100644 --- a/src/main/java/aquality/selenium/core/visualization/VisualStateProvider.java +++ b/src/main/java/aquality/selenium/core/visualization/VisualStateProvider.java @@ -1,43 +1,81 @@ package aquality.selenium.core.visualization; -import org.openqa.selenium.Dimension; -import org.openqa.selenium.OutputType; -import org.openqa.selenium.Point; +import aquality.selenium.core.logging.ILogVisualState; +import aquality.selenium.core.utilities.IElementActionRetrier; import org.openqa.selenium.remote.RemoteWebElement; -import javax.imageio.ImageIO; import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; +import java.util.function.Function; import java.util.function.Supplier; -import static java.awt.image.BufferedImage.TYPE_INT_RGB; - -public class VisualStateProvider { +public class VisualStateProvider implements IVisualStateProvider { + private final IImageComparator imageComparator; + private final IElementActionRetrier actionRetrier; private final Supplier getElement; + private final ILogVisualState stateLogger; - public VisualStateProvider(Supplier getElement){ + public VisualStateProvider(IImageComparator imageComparator, IElementActionRetrier actionRetrier, Supplier getElement, ILogVisualState stateLogger){ + this.imageComparator = imageComparator; + this.actionRetrier = actionRetrier; this.getElement = getElement; + this.stateLogger = stateLogger; } public Dimension getSize() { - return getElement.get().getSize(); + return getLoggedValue("size", element -> { + final org.openqa.selenium.Dimension size = element.getSize(); + return new Dimension(size.getWidth(), size.getHeight()); + }, null); } public Point getLocation() { - return getElement.get().getLocation(); + return getLoggedValue("location", element -> { + final org.openqa.selenium.Point location = element.getLocation(); + return new Point(location.getX(), location.getY()); + }, null); } public Image getImage() { - byte[] bytes = getElement.get().getScreenshotAs(OutputType.BYTES); - try (InputStream is = new ByteArrayInputStream(bytes)) { - return ImageIO.read(is); - } catch (IOException exception) { - //log - return new BufferedImage(0, 0, TYPE_INT_RGB); + return getLoggedValue("image", ImageFunctions::getScreenshotAsImage, + image -> getStringValue(ImageFunctions.getSize(image))); + } + + @Override + public float getDifference(Image theOtherOne, float threshold) { + Image currentImage = getImage(); + float value = 1; + stateLogger.logVisualState("loc.el.visual.getdifference.withthreshold", + getStringValue(ImageFunctions.getSize(theOtherOne)), threshold * 100); + if (!ImageFunctions.getSize(currentImage).equals(new Dimension())) { + value = imageComparator.percentageDifference(currentImage, theOtherOne, threshold); + } + stateLogger.logVisualState("loc.el.visual.difference.value", value * 100); + return value; + } + + @Override + public float getDifference(Image theOtherOne) { + Image currentImage = getImage(); + float value = 1; + stateLogger.logVisualState("loc.el.visual.getdifference", + getStringValue(ImageFunctions.getSize(theOtherOne))); + if (!ImageFunctions.getSize(currentImage).equals(new Dimension())) { + value = imageComparator.percentageDifference(currentImage, theOtherOne); } + stateLogger.logVisualState("loc.el.visual.difference.value", value * 100); + return value; + } + + private T getLoggedValue(String name, Function getValue, Function toString) { + stateLogger.logVisualState("loc.el.visual.get" + name); + T value = actionRetrier.doWithRetry(() -> getValue.apply(getElement.get())); + String stringValue = toString == null ? getStringValue(value) : toString.apply(value); + stateLogger.logVisualState(String.format("loc.el.visual.%s.value", name), stringValue); + return value; + } + + private String getStringValue(T value) { + return value.toString().replace(value.getClass().getName(), ""); } } diff --git a/src/main/resources/localization/core.be.json b/src/main/resources/localization/core.be.json index 49e0751..1b21067 100644 --- a/src/main/resources/localization/core.be.json +++ b/src/main/resources/localization/core.be.json @@ -19,5 +19,23 @@ "loc.el.state.enabled": "даступны", "loc.el.state.not.enabled": "недаступны", "loc.el.state.clickable": "даступны для націску", - "loc.get.page.source.failed": "Адбылася памылка падчас атрымання разметкі старонкі" -} \ No newline at end of file + "loc.get.page.source.failed": "Адбылася памылка падчас атрымання разметкі старонкі", + "loc.el.visual.getimage": "Атрымліваем малюнак элемента", + "loc.el.visual.image.value": "Памер малюнка элемента: %1$s", + "loc.el.visual.getlocation": "Атрымліваем месцазнаходжанне элемента на старонцы", + "loc.el.visual.location.value": "Месцазнаходжанне элемента на старонцы: %1$s", + "loc.el.visual.getsize": "Атрымліваем памер элемента", + "loc.el.visual.size.value": "Памер элемента: %1$s", + "loc.el.visual.getdifference": "Параўноўваем малюнак элемента з малюнкам памеру %1$s", + "loc.el.visual.getdifference.withthreshold": "Параўноўваем малюнак элемента з малюнкам памеру %s з парогам адчувальнасці [%.2f%%]", + "loc.el.visual.difference.value": "Адрозненне паміж цяперашнім і пададзеным малюнкамі складае [%.2f%%]", + "loc.form.dump.save": "Захоўваем дамп формы з іменем [%1$s]", + "loc.form.dump.exceededdumpname": "Перавышаны ліміт даўжыні імені дампа. Канечны шлях: [%1$s]", + "loc.form.dump.imagenotsaved": "Не ўдалося захаваць малюнак элемента [%1$s]: %2$s", + "loc.form.dump.compare": "Параўноўваем элементы формы з дампам [%1$s]", + "loc.form.dump.elementnotfound": "Элемент [%1$s], знойдзены ў дампе, не быў знойдзены на форме", + "loc.form.dump.elementsmissedindump": "Элементы, прысутныя на форме, не былі знойдзеныя ў дампе: [%1$s].", + "loc.form.dump.elementsmissedonform": "Элементы, прысутныя ў дампе, не былі знойдзеныя на форме: [%1$s].", + "loc.form.dump.unprocessedelements": "Колькасць неапрацаваных элементаў(не знойдзеных у дампе ці ў форме) складае [%d]. Для іх адрозненне прынята за 100%%", + "loc.form.dump.compare.result": "Адрозненне формы ад пададзенага дампа складае [%.2f%%]" +} diff --git a/src/main/resources/localization/core.en.json b/src/main/resources/localization/core.en.json index 7d99add..9d004d1 100644 --- a/src/main/resources/localization/core.en.json +++ b/src/main/resources/localization/core.en.json @@ -19,5 +19,23 @@ "loc.el.state.enabled": "enabled", "loc.el.state.not.enabled": "disabled", "loc.el.state.clickable": "clickable", - "loc.get.page.source.failed": "An exception occurred while tried to save the page source" + "loc.get.page.source.failed": "An exception occurred while tried to save the page source", + "loc.el.visual.getimage": "Getting image of element", + "loc.el.visual.image.value": "Element's image size: %1$s", + "loc.el.visual.getlocation": "Getting element location on the page", + "loc.el.visual.location.value": "Element location on the page: %1$s", + "loc.el.visual.getsize": "Getting element size", + "loc.el.visual.size.value": "Element size: %1$s", + "loc.el.visual.getdifference": "Comparing element's image to image of size %1$s", + "loc.el.visual.getdifference.withthreshold": "Comparing element's image to image of size %s with threshold [%.2f%%]", + "loc.el.visual.difference.value": "The difference between the current and the given images is [%.2f%%]", + "loc.form.dump.save": "Saving dump of the form with name [%1$s]", + "loc.form.dump.exceededdumpname": "Dump name length exceeded. Final path: [%1$s]", + "loc.form.dump.imagenotsaved": "Failed to save image of the element [%1$s]: %1$s", + "loc.form.dump.compare": "Comparing elements of the form to dump [%1$s]", + "loc.form.dump.elementnotfound": "Element [%1$s] found in the dump but was not found on form", + "loc.form.dump.elementsmissedindump": "Elements that were found on form but missed in the dump: [%1$s].", + "loc.form.dump.elementsmissedonform": "Elements that were found in the dump but missed on form: [%1$s].", + "loc.form.dump.unprocessedelements": "Count of unprocessed (no match between form and dump) elements is [%d]. For them difference counts as 100%%", + "loc.form.dump.compare.result": "The difference between the current form and the given dump is [%.2f%%]" } \ No newline at end of file diff --git a/src/main/resources/localization/core.pl.json b/src/main/resources/localization/core.pl.json index 4931f8a..3ef7acd 100644 --- a/src/main/resources/localization/core.pl.json +++ b/src/main/resources/localization/core.pl.json @@ -19,5 +19,23 @@ "loc.el.state.enabled": "dostępnym", "loc.el.state.not.enabled": "niedostępnym", "loc.el.state.clickable": "klikalnym", - "loc.get.page.source.failed": "Zapisywanie źródła strony nie powiodło się" -} \ No newline at end of file + "loc.get.page.source.failed": "Zapisywanie źródła strony nie powiodło się", + "loc.el.visual.getimage": "Uzyskanie obrazu elementu", + "loc.el.visual.image.value": "Rozmiar obrazu elementu: %1$s", + "loc.el.visual.getlocation": "Uzyskanie lokalizacji elementu na stronie", + "loc.el.visual.location.value": "Lokalizacja elementu na stronie: %1$s", + "loc.el.visual.getsize": "Uzyskanie rozmiaru elementu", + "loc.el.visual.size.value": "Rozmiar elementu: %1$s", + "loc.el.visual.getdifference": "Porównywanie obrazu elementu z obrazem o rozmiarze %1$s", + "loc.el.visual.getdifference.withthreshold": "Porównywanie obrazu elementu z obrazem o rozmiarze %s z zadanym progiem [%.2f%%]", + "loc.el.visual.difference.value": "Różnica między aktualnym a podanym obrazem to [%.2f%%]", + "loc.form.dump.save": "Zapisywanie zrzutu formularza o nazwie [%1$s]", + "loc.form.dump.exceededdumpname": "Przekroczono długość nazwy zrzutu. Ostateczna ścieżka pliku: [%1$s]", + "loc.form.dump.imagenotsaved": "Nie udało się zapisać obrazu elementu [%1$s]: %2$s", + "loc.form.dump.compare": "Porównywanie elementów formularza do zrzutu [%1$s]", + "loc.form.dump.elementnotfound": "Element [%1$s] znaleziony w zrzucie, nie został znaleziony w formularzu", + "loc.form.dump.elementsmissedindump": "Elementy, które zostały znalezione na formularzu, ale pominięte w zrzucie: [%1$s].", + "loc.form.dump.elementsmissedonform": "Elementy obecne w zrzucie, ale nie znalezione w formularzu: [%1$s].", + "loc.form.dump.unprocessedelements": "Ilość nieprzetworzonych elementów (brak dopasowania między formularzem a zrzutem) wynosi [%d]. Dla nich różnica liczy się jako 100%%", + "loc.form.dump.compare.result": "Różnica między aktualną formą a danym zrzutem wynosi [%.2f%%]" +} diff --git a/src/main/resources/localization/core.ru.json b/src/main/resources/localization/core.ru.json index a2257c0..f25e410 100644 --- a/src/main/resources/localization/core.ru.json +++ b/src/main/resources/localization/core.ru.json @@ -19,5 +19,23 @@ "loc.el.state.enabled": "доступным", "loc.el.state.not.enabled": "недоступным", "loc.el.state.clickable": "кликабельным", - "loc.get.page.source.failed": "Произошла ошибка во время получения разметки страницы" -} \ No newline at end of file + "loc.get.page.source.failed": "Произошла ошибка во время получения разметки страницы", + "loc.el.visual.getimage": "Получаем изображение элемента", + "loc.el.visual.image.value": "Размеры изображения элемента : %1$s", + "loc.el.visual.getlocation": "Получаем положение элемента на странице", + "loc.el.visual.location.value": "Положение элемента на странице: %1$s", + "loc.el.visual.getsize": "Получаем размер элемента", + "loc.el.visual.size.value": "Размер элемента: %1$s", + "loc.el.visual.getdifference": "Сравниваем изображение элемента с изображением размера %1$s", + "loc.el.visual.getdifference.withthreshold": "Сравниваем изображение элемента с изображением размера %s с уровнем шума [%.2f%%]", + "loc.el.visual.difference.value": "Отличие текущего изображения от предоставленного составляет [%.2f%%]", + "loc.form.dump.save": "Сохраняем дамп формы с именем [%1$s]", + "loc.form.dump.exceededdumpname": "Превышен лимит длины имени дампа. Конечный путь: %1$s", + "loc.form.dump.imagenotsaved": "Не удалось сохранить изображение элемента [%1$s]: %2$s", + "loc.form.dump.compare": "Сравниваем элементы формы с дампом [%1$s]", + "loc.form.dump.elementnotfound": "Элемент [%1$s], найденный в дампе, не был найден на форме", + "loc.form.dump.elementsmissedindump": "Элементы, найденные на форме, не были найдены в дампе: [%1$s].", + "loc.form.dump.elementsmissedonform": "Элементы, найденные в дампе, не были найдены на форме: [%1$s].", + "loc.form.dump.unprocessedelements": "Количество необработанных элементов(не найдено совпадения между дампом и формой) составило [%d]. Для них отличие принято за 100%%", + "loc.form.dump.compare.result": "Отличие формы от предоставленного дампа составляет [%.2f%%]" +} diff --git a/src/main/resources/localization/core.uk.json b/src/main/resources/localization/core.uk.json index d1235d5..f8b344a 100644 --- a/src/main/resources/localization/core.uk.json +++ b/src/main/resources/localization/core.uk.json @@ -19,5 +19,23 @@ "loc.el.state.enabled": "доступний", "loc.el.state.not.enabled": "недоступний", "loc.el.state.clickable": "доступний для натискання", - "loc.get.page.source.failed": "Не вдалося отримати розмітку сторінки" + "loc.get.page.source.failed": "Не вдалося отримати розмітку сторінки", + "loc.el.visual.getimage": "Отримання зображення елемента", + "loc.el.visual.image.value": "Розмір зображення елемента: %1$s", + "loc.el.visual.getlocation": "Отримання розташування елемента на сторінці", + "loc.el.visual.location.value": "Розташування елемента на сторінці: %1$s", + "loc.el.visual.getsize": "Отримання розміру елемента", + "loc.el.visual.size.value": "Розмір елемента: %1$s", + "loc.el.visual.getdifference": "Порівняння зображення елемента з зображенням розміру %1$s", + "loc.el.visual.getdifference.withthreshold": "Порівняння зображення елемента із зображенням розміру %s з порогом [%.2f%%]", + "loc.el.visual.difference.value": "Різниця між поточним і наданим зображеннями становить [%.2f%%]", + "loc.form.dump.save": "Збереження дампу форми з назвою [%1$s]", + "loc.form.dump.exceededdumpname": "Перевищено довжину назви дампу. Кінцевий шлях: [%1$s]", + "loc.form.dump.imagenotsaved": "Не вдалося зберегти зображення елемента [%1$s]: %2$s", + "loc.form.dump.compare": "Порівняння елементів форми з дампом [%1$s]", + "loc.form.dump.elementnotfound": "Елемент [%1$s] знайдено в дампі, але не знайдено на формі", + "loc.form.dump.elementsmissedindump": "Елементи, які були знайдені на формі, але пропущені в дампі: [%1$s].", + "loc.form.dump.elementsmissedonform": "Елементи, які були знайдені в дампі, але пропущені на формі: [%1$s].", + "loc.form.dump.unprocessedelements": "Кількість необроблених (без відповідності між формою і дампом) елементів становить [%d]. Для них різниця вважається 100%%", + "loc.form.dump.compare.result": "Різниця між поточною формою і даним дампом становить [%.2f%%]" } \ No newline at end of file diff --git a/src/main/resources/settings.json b/src/main/resources/settings.json index 6c8b630..a3c86bc 100644 --- a/src/main/resources/settings.json +++ b/src/main/resources/settings.json @@ -15,5 +15,13 @@ }, "elementCache": { "isEnabled": false + }, + "visualization": { + "imageExtension": "png", + "maxFullFileNameLength": 255, + "defaultThreshold": 0.012, + "comparisonWidth": 16, + "comparisonHeight": 16, + "pathToDumps": "../../../Resources/VisualDumps/" } } diff --git a/src/test/java/tests/applications/ICachedElement.java b/src/test/java/tests/applications/ICachedElement.java index 8c4a97b..6aada0d 100644 --- a/src/test/java/tests/applications/ICachedElement.java +++ b/src/test/java/tests/applications/ICachedElement.java @@ -6,7 +6,7 @@ import aquality.selenium.core.elements.interfaces.IElementCacheHandler; import aquality.selenium.core.elements.interfaces.IElementFinder; import aquality.selenium.core.elements.interfaces.IElementStateProvider; -import aquality.selenium.core.elements.interfaces.ILogElementState; +import aquality.selenium.core.logging.ILogElementState; import aquality.selenium.core.localization.ILocalizationManager; import aquality.selenium.core.localization.ILocalizedLogger; import aquality.selenium.core.waitings.IConditionalWait; diff --git a/src/test/java/tests/applications/browser/CachedLabel.java b/src/test/java/tests/applications/browser/CachedLabel.java index bfad7b4..d0f8ca9 100644 --- a/src/test/java/tests/applications/browser/CachedLabel.java +++ b/src/test/java/tests/applications/browser/CachedLabel.java @@ -5,6 +5,8 @@ import aquality.selenium.core.elements.interfaces.IElementFinder; import aquality.selenium.core.localization.ILocalizationManager; import aquality.selenium.core.localization.ILocalizedLogger; +import aquality.selenium.core.utilities.IElementActionRetrier; +import aquality.selenium.core.visualization.IImageComparator; import aquality.selenium.core.visualization.VisualStateProvider; import aquality.selenium.core.waitings.IConditionalWait; import org.openqa.selenium.By; @@ -61,6 +63,8 @@ public ILocalizationManager getLocalizationManager() { } public VisualStateProvider visual() { - return new VisualStateProvider(this::getElement); + return new VisualStateProvider(AqualityServices.get(IImageComparator.class), + AqualityServices.get(IElementActionRetrier.class), this::getElement, (messageKey, args) -> + getLocalizedLogger().infoElementAction(CachedLabel.class.getSimpleName(), getLocator().toString(), messageKey, args)); } } diff --git a/src/test/java/tests/applications/browser/ChromeApplication.java b/src/test/java/tests/applications/browser/ChromeApplication.java index c7bccac..3305a37 100644 --- a/src/test/java/tests/applications/browser/ChromeApplication.java +++ b/src/test/java/tests/applications/browser/ChromeApplication.java @@ -6,14 +6,13 @@ import org.openqa.selenium.remote.RemoteWebDriver; import java.time.Duration; -import java.util.concurrent.TimeUnit; public class ChromeApplication implements IApplication { private Duration implicitWait; private final RemoteWebDriver driver; ChromeApplication(long implicitWaitSeconds) { - driver = new ChromeDriver(new ChromeOptions().setHeadless(true)); + driver = new ChromeDriver(new ChromeOptions().addArguments("--headless")); setImplicitWaitTimeout(Duration.ofSeconds(implicitWaitSeconds)); } @@ -30,7 +29,7 @@ public boolean isStarted() { @Override public void setImplicitWaitTimeout(Duration value) { if (implicitWait != value){ - driver.manage().timeouts().implicitlyWait(value.getSeconds(), TimeUnit.SECONDS); + driver.manage().timeouts().implicitlyWait(value); implicitWait = value; } } diff --git a/src/test/java/tests/elements/factory/CustomElement.java b/src/test/java/tests/elements/factory/CustomElement.java index 9e7cc3f..b1f0fe5 100644 --- a/src/test/java/tests/elements/factory/CustomElement.java +++ b/src/test/java/tests/elements/factory/CustomElement.java @@ -9,6 +9,7 @@ import aquality.selenium.core.localization.ILocalizationManager; import aquality.selenium.core.localization.ILocalizedLogger; import aquality.selenium.core.utilities.IElementActionRetrier; +import aquality.selenium.core.visualization.IImageComparator; import aquality.selenium.core.waitings.IConditionalWait; import org.openqa.selenium.By; import tests.applications.windowsApp.AqualityServices; @@ -34,6 +35,11 @@ protected IElementFinder getElementFinder() { return AqualityServices.get(IElementFinder.class); } + @Override + protected IImageComparator getImageComparator() { + return AqualityServices.get(IImageComparator.class); + } + @Override protected IElementCacheConfiguration getElementCacheConfiguration() { return AqualityServices.get(IElementCacheConfiguration.class); diff --git a/src/test/java/tests/elements/factory/CustomWebElement.java b/src/test/java/tests/elements/factory/CustomWebElement.java index 258c692..df37af6 100644 --- a/src/test/java/tests/elements/factory/CustomWebElement.java +++ b/src/test/java/tests/elements/factory/CustomWebElement.java @@ -9,6 +9,7 @@ import aquality.selenium.core.localization.ILocalizationManager; import aquality.selenium.core.localization.ILocalizedLogger; import aquality.selenium.core.utilities.IElementActionRetrier; +import aquality.selenium.core.visualization.IImageComparator; import aquality.selenium.core.waitings.IConditionalWait; import org.openqa.selenium.By; import tests.applications.browser.AqualityServices; @@ -34,6 +35,11 @@ protected IElementFinder getElementFinder() { return AqualityServices.get(IElementFinder.class); } + @Override + protected IImageComparator getImageComparator() { + return AqualityServices.get(IImageComparator.class); + } + @Override protected IElementCacheConfiguration getElementCacheConfiguration() { return AqualityServices.get(IElementCacheConfiguration.class); diff --git a/src/test/java/tests/visualization/ImageComparatorTests.java b/src/test/java/tests/visualization/ImageComparatorTests.java index 7f5982b..2dd3b6a 100644 --- a/src/test/java/tests/visualization/ImageComparatorTests.java +++ b/src/test/java/tests/visualization/ImageComparatorTests.java @@ -1,6 +1,7 @@ package tests.visualization; import aquality.selenium.core.visualization.IImageComparator; +import aquality.selenium.core.visualization.ImageFunctions; import org.testng.Assert; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @@ -18,6 +19,14 @@ private void startLoading() { DynamicLoadingForm.getStartLabel().click(); } + private Image getStartImage() { + return ImageFunctions.getScreenshotAsImage(DynamicLoadingForm.getStartLabel().getElement()); + } + + private Image getLoadingImage() { + return ImageFunctions.getScreenshotAsImage(DynamicLoadingForm.getLoadingLabel().getElement()); + } + @Override @BeforeMethod public void beforeMethod() { @@ -26,25 +35,25 @@ public void beforeMethod() { @Test public void testGetPercentageDifferenceForSameElement() { - Image firstImage = DynamicLoadingForm.getStartLabel().visual().getImage(); - Image secondImage = DynamicLoadingForm.getStartLabel().visual().getImage(); + Image firstImage = getStartImage(); + Image secondImage = getStartImage(); Assert.assertEquals(imageComparator.percentageDifference(firstImage, secondImage), 0); } @Test public void testGetPercentageDifferenceForSameElementWithZeroThreshold() { - Image firstImage = DynamicLoadingForm.getStartLabel().visual().getImage(); - Image secondImage = DynamicLoadingForm.getStartLabel().visual().getImage(); + Image firstImage = getStartImage(); + Image secondImage = getStartImage(); Assert.assertEquals(imageComparator.percentageDifference(firstImage, secondImage, 0), 0); } @Test public void testGetPercentageDifferenceForDifferentElements() { - Image firstImage = DynamicLoadingForm.getStartLabel().visual().getImage(); + Image firstImage = getStartImage(); startLoading(); - Image secondImage = DynamicLoadingForm.getLoadingLabel().visual().getImage(); + Image secondImage = getLoadingImage(); Assert.assertNotEquals(imageComparator.percentageDifference(firstImage, secondImage), 0); } @@ -52,9 +61,9 @@ public void testGetPercentageDifferenceForDifferentElements() { @Test public void testGetPercentageDifferenceForDifferentElementsWithFullThreshold() { final int threshold = 1; - Image firstImage = DynamicLoadingForm.getStartLabel().visual().getImage(); + Image firstImage = getStartImage(); startLoading(); - Image secondImage = DynamicLoadingForm.getLoadingLabel().visual().getImage(); + Image secondImage = getLoadingImage(); Assert.assertEquals(imageComparator.percentageDifference(firstImage, secondImage, threshold), 0); } @@ -62,9 +71,9 @@ public void testGetPercentageDifferenceForDifferentElementsWithFullThreshold() { @Test public void testGetPercentageDifferenceForSimilarElements() throws InterruptedException { startLoading(); - Image firstImage = DynamicLoadingForm.getLoadingLabel().visual().getImage(); + Image firstImage = getLoadingImage(); Thread.sleep(300); - Image secondImage = DynamicLoadingForm.getLoadingLabel().visual().getImage(); + Image secondImage = getLoadingImage(); Assert.assertTrue(imageComparator.percentageDifference(firstImage, secondImage, 0) != 0, "With zero threshold, there should be some difference"); diff --git a/src/test/java/tests/visualization/VisualStateProviderTests.java b/src/test/java/tests/visualization/VisualStateProviderTests.java new file mode 100644 index 0000000..5ed2a51 --- /dev/null +++ b/src/test/java/tests/visualization/VisualStateProviderTests.java @@ -0,0 +1,91 @@ +package tests.visualization; + +import aquality.selenium.core.visualization.IVisualStateProvider; +import aquality.selenium.core.visualization.ImageFunctions; +import org.testng.Assert; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import tests.applications.browser.ITheInternetPageTest; +import theinternet.DynamicLoadingForm; +import theinternet.TheInternetPage; + +import java.awt.*; + +public class VisualStateProviderTests implements ITheInternetPageTest { + private void startLoading() { + DynamicLoadingForm.getStartLabel().click(); + } + + private IVisualStateProvider getStartVisual() { + return DynamicLoadingForm.getStartLabel().visual(); + } + + private IVisualStateProvider getLoadingVisual() { + return DynamicLoadingForm.getLoadingLabel().visual(); + } + + @Override + @BeforeMethod + public void beforeMethod() { + navigate(TheInternetPage.DYNAMIC_LOADING); + } + + @Test + public void testGetElementSize() { + Dimension actualSize = getStartVisual().getSize(); + Assert.assertNotNull(actualSize); + Assert.assertNotEquals(actualSize, new Dimension()); + } + + @Test + public void testGetElementLocation() { + Point actualLocation = getStartVisual().getLocation(); + Assert.assertNotNull(actualLocation); + Assert.assertNotEquals(actualLocation, new Point()); + } + + @Test + public void testGetElementImage() { + Image actualImage = getStartVisual().getImage(); + Assert.assertNotNull(actualImage); + Assert.assertNotEquals(ImageFunctions.getSize(actualImage), new Dimension()); + } + + @Test + public void testGetPercentageDifferenceForSameElement() { + Image firstImage = getStartVisual().getImage(); + Assert.assertEquals(getStartVisual().getDifference(firstImage), 0); + } + + @Test + public void testGetPercentageDifferenceForSameElementWithZeroThreshold() { + Image firstImage = getStartVisual().getImage(); + Assert.assertEquals(getStartVisual().getDifference(firstImage, 0), 0); + } + + @Test + public void testGetPercentageDifferenceForDifferentElements() { + Image firstImage = getStartVisual().getImage(); + startLoading(); + Assert.assertNotEquals(getLoadingVisual().getDifference(firstImage), 0); + } + + @Test + public void testGetPercentageDifferenceForDifferentElementsWithFullThreshold() { + Image firstImage = getStartVisual().getImage(); + startLoading(); + Assert.assertEquals(getLoadingVisual().getDifference(firstImage, 1), 0); + } + + @Test + public void testGetPercentageDifferenceForSimilarElements() throws InterruptedException { + startLoading(); + Image firstImage = getLoadingVisual().getImage(); + Thread.sleep(100); + Assert.assertNotEquals(getLoadingVisual().getDifference(firstImage, 0), 0); + Assert.assertTrue(getLoadingVisual().getDifference(firstImage, 0.2f) <= 0.3); + Assert.assertTrue(getLoadingVisual().getDifference(firstImage, 0.4f) <= 0.2); + Assert.assertTrue(getLoadingVisual().getDifference(firstImage, 0.6f) <= 0.1); + Assert.assertEquals(getLoadingVisual().getDifference(firstImage, 0.8f), 0); + } +} From 2274f821962feb4dea66531afb726ae131c07cc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alaksiej=20Miale=C5=A1ka?= Date: Thu, 9 Mar 2023 23:11:38 +0100 Subject: [PATCH 03/12] Implement VisualConfiguration --- .../configurations/IVisualConfiguration.java | 30 +++++++++ .../configurations/VisualConfiguration.java | 62 +++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/src/main/java/aquality/selenium/core/configurations/IVisualConfiguration.java b/src/main/java/aquality/selenium/core/configurations/IVisualConfiguration.java index d062bfb..c2941fb 100644 --- a/src/main/java/aquality/selenium/core/configurations/IVisualConfiguration.java +++ b/src/main/java/aquality/selenium/core/configurations/IVisualConfiguration.java @@ -9,4 +9,34 @@ public interface IVisualConfiguration { * @return image format. */ String getImageFormat(); + + /** + * Gets maximum length of full file name with path for image comparison. + * @return maximum symbols count in file path. + */ + int getMaxFullFileNameLength(); + + /** + * Gets default threshold used for image comparison. + * @return The default threshold value. + */ + float getDefaultThreshold(); + + /** + * Gets width of the image resized for comparison. + * @return comparison width. + */ + int getComparisonWidth(); + + /** + * Gets height of the image resized for comparison. + * @return comparison height. + */ + int getComparisonHeight(); + + /** + * Gets path used to save and load page dumps. + * @return path to dumps. + */ + String getPathToDumps(); } diff --git a/src/main/java/aquality/selenium/core/configurations/VisualConfiguration.java b/src/main/java/aquality/selenium/core/configurations/VisualConfiguration.java index 5ef1323..dedce63 100644 --- a/src/main/java/aquality/selenium/core/configurations/VisualConfiguration.java +++ b/src/main/java/aquality/selenium/core/configurations/VisualConfiguration.java @@ -1,9 +1,12 @@ package aquality.selenium.core.configurations; +import aquality.selenium.core.logging.Logger; import aquality.selenium.core.utilities.ISettingsFile; import com.google.inject.Inject; import javax.imageio.ImageIO; +import java.io.File; +import java.io.IOException; import java.util.Arrays; /** @@ -12,6 +15,12 @@ */ public class VisualConfiguration implements IVisualConfiguration { private String imageFormat; + private Integer maxFullFileNameLength; + private Float defaultThreshold; + private Integer comparisonWidth; + private Integer comparisonHeight; + private String pathToDumps; + private final ISettingsFile settingsFile; /** @@ -37,4 +46,57 @@ public String getImageFormat() { } return imageFormat; } + + @Override + public int getMaxFullFileNameLength() { + if (maxFullFileNameLength == null) { + maxFullFileNameLength = Integer.valueOf( + settingsFile.getValueOrDefault("/visualization/maxFullFileNameLength", 255).toString()); + } + return maxFullFileNameLength; + } + + @Override + public float getDefaultThreshold() { + if (defaultThreshold == null) { + defaultThreshold = Float.valueOf( + settingsFile.getValueOrDefault("/visualization/defaultThreshold", 0.012f).toString()); + } + return defaultThreshold; + } + + @Override + public int getComparisonWidth() { + if (comparisonWidth == null) { + comparisonWidth = Integer.valueOf( + settingsFile.getValueOrDefault("/visualization/comparisonWidth", 16).toString()); + } + return comparisonWidth; + } + + @Override + public int getComparisonHeight() { + if (comparisonHeight == null) { + comparisonHeight = Integer.valueOf( + settingsFile.getValueOrDefault("/visualization/comparisonHeight", 16).toString()); + } + return comparisonHeight; + } + + @Override + public String getPathToDumps() { + if (pathToDumps == null) { + pathToDumps = settingsFile.getValueOrDefault(".visualization.pathToDumps", "./src/test/resources/VisualDumps/").toString(); + if (pathToDumps.startsWith(".")) { + try { + pathToDumps = new File(pathToDumps).getCanonicalPath(); + } catch (IOException e) { + String errorMessage = "Failed to resolve path to dumps: " + e.getMessage(); + Logger.getInstance().fatal(errorMessage, e); + throw new IllegalArgumentException(errorMessage, e); + } + } + } + return pathToDumps; + } } From 30938e95dc6787065011e6e287f4bbfbe859281f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alaksiej=20Miale=C5=A1ka?= Date: Thu, 9 Mar 2023 23:12:12 +0100 Subject: [PATCH 04/12] Fix typo in ElementsCount enum --- .../java/aquality/selenium/core/elements/ElementFactory.java | 2 +- .../java/aquality/selenium/core/elements/ElementsCount.java | 2 +- src/test/java/tests/elements/factory/IFindElementsTests.java | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/aquality/selenium/core/elements/ElementFactory.java b/src/main/java/aquality/selenium/core/elements/ElementFactory.java index f0d408e..fcb4c52 100644 --- a/src/main/java/aquality/selenium/core/elements/ElementFactory.java +++ b/src/main/java/aquality/selenium/core/elements/ElementFactory.java @@ -106,7 +106,7 @@ protected void waitForElementsCount(By locator, ElementsCount count, ElementStat localizationManager.getLocalizedMessage("loc.elements.found.but.should.not", locator.toString(), state.toString())); break; - case MORE_THEN_ZERO: + case MORE_THAN_ZERO: conditionalWait.waitForTrue(() -> !elementFinder.findElements(locator, state, ZERO_TIMEOUT).isEmpty(), localizationManager.getLocalizedMessage("loc.no.elements.found.by.locator", locator.toString(), state.toString())); diff --git a/src/main/java/aquality/selenium/core/elements/ElementsCount.java b/src/main/java/aquality/selenium/core/elements/ElementsCount.java index c400bcf..edb0f9f 100644 --- a/src/main/java/aquality/selenium/core/elements/ElementsCount.java +++ b/src/main/java/aquality/selenium/core/elements/ElementsCount.java @@ -2,6 +2,6 @@ public enum ElementsCount { ZERO, - MORE_THEN_ZERO, + MORE_THAN_ZERO, ANY } diff --git a/src/test/java/tests/elements/factory/IFindElementsTests.java b/src/test/java/tests/elements/factory/IFindElementsTests.java index 13d9d48..1327069 100644 --- a/src/test/java/tests/elements/factory/IFindElementsTests.java +++ b/src/test/java/tests/elements/factory/IFindElementsTests.java @@ -77,13 +77,13 @@ default void shouldBePossibleToFindCustomElementsViaCustomFactory() { @Test default void shouldBePossibleToFindCustomElementsViaCustomFactoryWithCustomElementsCount() { - Assert.assertTrue(findElements(CalculatorWindow.getEqualsButtonByXPath(), ICustomElement.class, ElementsCount.MORE_THEN_ZERO).size() > 1); + Assert.assertTrue(findElements(CalculatorWindow.getEqualsButtonByXPath(), ICustomElement.class, ElementsCount.MORE_THAN_ZERO).size() > 1); } @Test default void shouldBePossibleToFindCustomElementsViaSupplierWithDefaultName() { Assert.assertTrue(findElements( - CalculatorWindow.getEqualsButtonByXPath(), CustomElement::new, ElementsCount.MORE_THEN_ZERO, + CalculatorWindow.getEqualsButtonByXPath(), CustomElement::new, ElementsCount.MORE_THAN_ZERO, ElementState.EXISTS_IN_ANY_STATE).size() > 1); } From 26af42e72f6f86a31e7b7959f495907e1675e640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alaksiej=20Miale=C5=A1ka?= Date: Thu, 9 Mar 2023 23:24:49 +0100 Subject: [PATCH 05/12] Rework grayscale and resizing ImageFunctions for ImageComparator Fix documentation for IVisualStateProvider Fix coding issues --- .../visualization/IVisualStateProvider.java | 1 - .../core/visualization/ImageComparator.java | 44 ++++++++++------- .../core/visualization/ImageFunctions.java | 48 ++++++++++++++++++- .../VisualStateProviderTests.java | 5 +- 4 files changed, 78 insertions(+), 20 deletions(-) diff --git a/src/main/java/aquality/selenium/core/visualization/IVisualStateProvider.java b/src/main/java/aquality/selenium/core/visualization/IVisualStateProvider.java index 7fb2b82..957910c 100644 --- a/src/main/java/aquality/selenium/core/visualization/IVisualStateProvider.java +++ b/src/main/java/aquality/selenium/core/visualization/IVisualStateProvider.java @@ -30,7 +30,6 @@ public interface IVisualStateProvider { * Gets the difference between the image of the element and the provided image using {@link IImageComparator}. * @param theOtherOne the image to compare the element with. * @param threshold how big a difference will be ignored as a percentage - value between 0 and 1. - * If the value is null, the default value is got from {@link aquality.selenium.core.configurations.IVisualConfiguration}. * @return the difference between the two images as a percentage - value between 0 and 1. */ float getDifference(Image theOtherOne, float threshold); diff --git a/src/main/java/aquality/selenium/core/visualization/ImageComparator.java b/src/main/java/aquality/selenium/core/visualization/ImageComparator.java index 151a1af..878cdcc 100644 --- a/src/main/java/aquality/selenium/core/visualization/ImageComparator.java +++ b/src/main/java/aquality/selenium/core/visualization/ImageComparator.java @@ -1,25 +1,39 @@ package aquality.selenium.core.visualization; +import aquality.selenium.core.configurations.IVisualConfiguration; +import com.google.inject.Inject; + import java.awt.*; import java.awt.image.BufferedImage; public class ImageComparator implements IImageComparator { - private static final int DEFAULT_THRESHOLD = 3; private static final int THRESHOLD_DIVISOR = 255; - private final int comparisonHeight = 16; - private final int comparisonWidth = 16; + private final IVisualConfiguration visualConfiguration; + + @Inject + public ImageComparator(IVisualConfiguration visualConfiguration) { + this.visualConfiguration = visualConfiguration; + } + + private int getComparisonHeight() { + return visualConfiguration.getComparisonHeight(); + } + + private int getComparisonWidth() { + return visualConfiguration.getComparisonWidth(); + } public float percentageDifference(Image thisOne, Image theOtherOne, float threshold) { if (threshold < 0 || threshold > 1) { throw new IllegalArgumentException(String.format("Threshold should be between 0 and 1, but was [%s]", threshold)); } - int intThreshold = Float.valueOf(threshold * THRESHOLD_DIVISOR).intValue(); + int intThreshold = (int) (threshold * THRESHOLD_DIVISOR); return percentageDifference(thisOne, theOtherOne, intThreshold); } public float percentageDifference(Image thisOne, Image theOtherOne) { - return percentageDifference(thisOne, theOtherOne, DEFAULT_THRESHOLD); + return percentageDifference(thisOne, theOtherOne, visualConfiguration.getDefaultThreshold()); } protected float percentageDifference(Image thisOne, Image theOtherOne, int threshold) { @@ -35,16 +49,16 @@ protected float percentageDifference(Image thisOne, Image theOtherOne, int thres } } - return diffPixels / (float) (comparisonWidth * comparisonHeight); + return diffPixels / (float) (getComparisonWidth() * getComparisonHeight()); } protected int[][] getDifferences(Image thisOne, Image theOtherOne) { int[][] firstGray = getResizedGrayScaleValues(thisOne); int[][] secondGray = getResizedGrayScaleValues(theOtherOne); - int[][] differences = new int[comparisonWidth][comparisonHeight]; - for (int y = 0; y < comparisonHeight; y++) { - for (int x = 0; x < comparisonWidth; x++) { + int[][] differences = new int[getComparisonWidth()][getComparisonHeight()]; + for (int y = 0; y < getComparisonHeight(); y++) { + for (int x = 0; x < getComparisonWidth(); x++) { differences[x][y] = (byte) Math.abs(firstGray[x][y] - secondGray[x][y]); } } @@ -53,13 +67,11 @@ protected int[][] getDifferences(Image thisOne, Image theOtherOne) { } protected int[][] getResizedGrayScaleValues(Image image) { - BufferedImage resizedImage = new BufferedImage(comparisonWidth, comparisonHeight, BufferedImage.TYPE_BYTE_GRAY); - Graphics2D graphics2D = resizedImage.createGraphics(); - graphics2D.drawImage(image, 0, 0, comparisonWidth, comparisonHeight, null); - graphics2D.dispose(); - int[][] grayScale = new int[comparisonWidth][comparisonHeight]; - for (int y = 0; y < comparisonHeight; y++) { - for (int x = 0; x < comparisonWidth; x++) { + BufferedImage resizedImage = ImageFunctions.resize(image, getComparisonWidth(), getComparisonHeight()); + BufferedImage grayImage = ImageFunctions.grayscale(resizedImage); + int[][] grayScale = new int[grayImage.getWidth()][grayImage.getHeight()]; + for (int y = 0; y < grayImage.getHeight(); y++) { + for (int x = 0; x < grayImage.getWidth(); x++) { int pixel = resizedImage.getRGB(x, y); int red = (pixel >> 16) & 0xff; grayScale[x][y] = Math.abs(red); diff --git a/src/main/java/aquality/selenium/core/visualization/ImageFunctions.java b/src/main/java/aquality/selenium/core/visualization/ImageFunctions.java index c1fef52..15378c1 100644 --- a/src/main/java/aquality/selenium/core/visualization/ImageFunctions.java +++ b/src/main/java/aquality/selenium/core/visualization/ImageFunctions.java @@ -19,8 +19,13 @@ * Image and screenshot extensions. */ public class ImageFunctions { + private ImageFunctions() throws InstantiationException { + throw new InstantiationException("Static ImageFunctions should not be initialized"); + } + /** * Represents given element's screenshot as an image. + * * @param element given element. * @return image object. */ @@ -30,6 +35,7 @@ public static BufferedImage getScreenshotAsImage(IElement element) { /** * Represents given element's screenshot as an image. + * * @param element given element. * @return image object. */ @@ -45,6 +51,7 @@ public static BufferedImage getScreenshotAsImage(RemoteWebElement element) { /** * Represents dimension of the given image. + * * @param image given image. * @return size of the given image. */ @@ -52,10 +59,47 @@ public static Dimension getSize(Image image) { if (image instanceof RenderedImage) { RenderedImage renderedImage = (RenderedImage) image; return new Dimension(renderedImage.getWidth(), renderedImage.getHeight()); - } - else { + } else { return new Dimension(image.getWidth(null), image.getHeight(null)); } + } + /** + * Gray-scaling the image. + * + * @param image source image. + * @return gray-scaled image. + */ + public static BufferedImage grayscale(BufferedImage image) { + BufferedImage result = new BufferedImage(image.getWidth(), image.getHeight(), image.getType()); + for (int y = 0; y < result.getHeight(); y++) { + for (int x = 0; x < result.getWidth(); x++) { + int pixel = image.getRGB(x, y); + int a = (pixel >> 24) & 0xff; + int r = (pixel >> 16) & 0xff; + int g = (pixel >> 8) & 0xff; + int b = pixel & 0xff; + int average = (r + g + b) / 3; + pixel = (a << 24) | (average << 16) | (average << 8) | average; + result.setRGB(x, y, pixel); + } + } + return result; + } + + /** + * Resizes image giving higher priority to image smoothness than scaling speed. + * @param image source image. + * @param width target width. + * @param height target height. + * @return resized image. + */ + public static BufferedImage resize(Image image, int width, int height) { + BufferedImage resizedImage = new BufferedImage(width, height, TYPE_INT_RGB); + Graphics graphics = resizedImage.getGraphics(); + Image scaledImage = image.getScaledInstance(width, height, Image.SCALE_SMOOTH); + graphics.drawImage(scaledImage, 0, 0, width, height, null); + graphics.dispose(); + return resizedImage; } } diff --git a/src/test/java/tests/visualization/VisualStateProviderTests.java b/src/test/java/tests/visualization/VisualStateProviderTests.java index 5ed2a51..cba0d31 100644 --- a/src/test/java/tests/visualization/VisualStateProviderTests.java +++ b/src/test/java/tests/visualization/VisualStateProviderTests.java @@ -2,9 +2,11 @@ import aquality.selenium.core.visualization.IVisualStateProvider; import aquality.selenium.core.visualization.ImageFunctions; +import aquality.selenium.core.waitings.IConditionalWait; import org.testng.Assert; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; +import tests.applications.browser.AqualityServices; import tests.applications.browser.ITheInternetPageTest; import theinternet.DynamicLoadingForm; import theinternet.TheInternetPage; @@ -81,7 +83,8 @@ public void testGetPercentageDifferenceForDifferentElementsWithFullThreshold() { public void testGetPercentageDifferenceForSimilarElements() throws InterruptedException { startLoading(); Image firstImage = getLoadingVisual().getImage(); - Thread.sleep(100); + AqualityServices.get(IConditionalWait.class).waitFor( + () -> firstImage.getHeight(null) < getLoadingVisual().getSize().getHeight()); Assert.assertNotEquals(getLoadingVisual().getDifference(firstImage, 0), 0); Assert.assertTrue(getLoadingVisual().getDifference(firstImage, 0.2f) <= 0.3); Assert.assertTrue(getLoadingVisual().getDifference(firstImage, 0.4f) <= 0.2); From f807f0e4df10f5450a658fe6309a87b1ce9bd37b Mon Sep 17 00:00:00 2001 From: Aleksey2 Meleshko Date: Fri, 10 Mar 2023 11:54:33 +0100 Subject: [PATCH 06/12] Fix codesmell in tests --- .../java/tests/visualization/ImageComparatorTests.java | 4 ++-- .../java/tests/visualization/VisualStateProviderTests.java | 7 ++----- src/test/java/theinternet/DynamicLoadingForm.java | 7 +++++++ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/test/java/tests/visualization/ImageComparatorTests.java b/src/test/java/tests/visualization/ImageComparatorTests.java index 2dd3b6a..d0de71d 100644 --- a/src/test/java/tests/visualization/ImageComparatorTests.java +++ b/src/test/java/tests/visualization/ImageComparatorTests.java @@ -69,10 +69,10 @@ public void testGetPercentageDifferenceForDifferentElementsWithFullThreshold() { } @Test - public void testGetPercentageDifferenceForSimilarElements() throws InterruptedException { + public void testGetPercentageDifferenceForSimilarElements() { startLoading(); Image firstImage = getLoadingImage(); - Thread.sleep(300); + DynamicLoadingForm.waitUntilLoaderChanged(firstImage.getHeight(null)); Image secondImage = getLoadingImage(); Assert.assertTrue(imageComparator.percentageDifference(firstImage, secondImage, 0) != 0, diff --git a/src/test/java/tests/visualization/VisualStateProviderTests.java b/src/test/java/tests/visualization/VisualStateProviderTests.java index cba0d31..1ebff3e 100644 --- a/src/test/java/tests/visualization/VisualStateProviderTests.java +++ b/src/test/java/tests/visualization/VisualStateProviderTests.java @@ -2,11 +2,9 @@ import aquality.selenium.core.visualization.IVisualStateProvider; import aquality.selenium.core.visualization.ImageFunctions; -import aquality.selenium.core.waitings.IConditionalWait; import org.testng.Assert; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; -import tests.applications.browser.AqualityServices; import tests.applications.browser.ITheInternetPageTest; import theinternet.DynamicLoadingForm; import theinternet.TheInternetPage; @@ -80,11 +78,10 @@ public void testGetPercentageDifferenceForDifferentElementsWithFullThreshold() { } @Test - public void testGetPercentageDifferenceForSimilarElements() throws InterruptedException { + public void testGetPercentageDifferenceForSimilarElements() { startLoading(); Image firstImage = getLoadingVisual().getImage(); - AqualityServices.get(IConditionalWait.class).waitFor( - () -> firstImage.getHeight(null) < getLoadingVisual().getSize().getHeight()); + DynamicLoadingForm.waitUntilLoaderChanged(firstImage.getHeight(null)); Assert.assertNotEquals(getLoadingVisual().getDifference(firstImage, 0), 0); Assert.assertTrue(getLoadingVisual().getDifference(firstImage, 0.2f) <= 0.3); Assert.assertTrue(getLoadingVisual().getDifference(firstImage, 0.4f) <= 0.2); diff --git a/src/test/java/theinternet/DynamicLoadingForm.java b/src/test/java/theinternet/DynamicLoadingForm.java index aa903a5..8449e80 100644 --- a/src/test/java/theinternet/DynamicLoadingForm.java +++ b/src/test/java/theinternet/DynamicLoadingForm.java @@ -3,7 +3,9 @@ import aquality.selenium.core.applications.IApplication; import aquality.selenium.core.elements.ElementState; import aquality.selenium.core.elements.interfaces.IElementStateProvider; +import aquality.selenium.core.waitings.IConditionalWait; import org.openqa.selenium.By; +import tests.applications.browser.AqualityServices; import tests.applications.browser.CachedLabel; import java.time.Duration; @@ -48,4 +50,9 @@ public IElementStateProvider loaderState() { public IElementStateProvider startButtonState() { return state(START_BUTTON_LOCATOR); } + + public static void waitUntilLoaderChanged(int oldHeight) { + AqualityServices.get(IConditionalWait.class).waitFor( + () -> oldHeight < getLoadingLabel().visual().getSize().getHeight()); + } } From cdcc63e723ef0278644849684235c1f9d4ee4457 Mon Sep 17 00:00:00 2001 From: Aleksey2 Meleshko Date: Fri, 10 Mar 2023 16:24:04 +0100 Subject: [PATCH 07/12] Add override annotations --- .../core/visualization/ImageComparator.java | 10 ++++--- .../core/visualization/ImageFunctions.java | 30 +++++++++++++++++-- .../visualization/VisualStateProvider.java | 4 ++- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/main/java/aquality/selenium/core/visualization/ImageComparator.java b/src/main/java/aquality/selenium/core/visualization/ImageComparator.java index 878cdcc..cc36a8b 100644 --- a/src/main/java/aquality/selenium/core/visualization/ImageComparator.java +++ b/src/main/java/aquality/selenium/core/visualization/ImageComparator.java @@ -23,6 +23,12 @@ private int getComparisonWidth() { return visualConfiguration.getComparisonWidth(); } + @Override + public float percentageDifference(Image thisOne, Image theOtherOne) { + return percentageDifference(thisOne, theOtherOne, visualConfiguration.getDefaultThreshold()); + } + + @Override public float percentageDifference(Image thisOne, Image theOtherOne, float threshold) { if (threshold < 0 || threshold > 1) { throw new IllegalArgumentException(String.format("Threshold should be between 0 and 1, but was [%s]", threshold)); @@ -32,10 +38,6 @@ public float percentageDifference(Image thisOne, Image theOtherOne, float thresh return percentageDifference(thisOne, theOtherOne, intThreshold); } - public float percentageDifference(Image thisOne, Image theOtherOne) { - return percentageDifference(thisOne, theOtherOne, visualConfiguration.getDefaultThreshold()); - } - protected float percentageDifference(Image thisOne, Image theOtherOne, int threshold) { int[][] differences = getDifferences(thisOne, theOtherOne); diff --git a/src/main/java/aquality/selenium/core/visualization/ImageFunctions.java b/src/main/java/aquality/selenium/core/visualization/ImageFunctions.java index 15378c1..4108126 100644 --- a/src/main/java/aquality/selenium/core/visualization/ImageFunctions.java +++ b/src/main/java/aquality/selenium/core/visualization/ImageFunctions.java @@ -5,13 +5,15 @@ import org.openqa.selenium.OutputType; import org.openqa.selenium.remote.RemoteWebElement; +import javax.imageio.IIOImage; import javax.imageio.ImageIO; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.stream.FileImageInputStream; import java.awt.*; import java.awt.image.BufferedImage; import java.awt.image.RenderedImage; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; +import java.io.*; import static java.awt.image.BufferedImage.TYPE_INT_RGB; @@ -102,4 +104,26 @@ public static BufferedImage resize(Image image, int width, int height) { graphics.dispose(); return resizedImage; } + + /** + * Saves image in the highest quality. + * @param image source image. + * @param name target name without extension. + * @param format target format. + */ + public static void save(RenderedImage image, String name, String format) { + final float highestQuality = 1.0F; + ImageWriter writer = ImageIO.getImageWritersByFormatName(format).next(); + ImageWriteParam param = writer.getDefaultWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionQuality(highestQuality); + String fileName = String.format("%s.%s", name, format.startsWith(".") ? format.substring(1) : format); + try { + writer.setOutput(new FileImageInputStream(new File(fileName))); + writer.write(null, new IIOImage(image, null, null), param); + } catch (IOException e) { + Logger.getInstance().fatal("Failed to save the image: " + e.getMessage(), e); + throw new UncheckedIOException(e); + } + } } diff --git a/src/main/java/aquality/selenium/core/visualization/VisualStateProvider.java b/src/main/java/aquality/selenium/core/visualization/VisualStateProvider.java index 1f0a055..4b1bf72 100644 --- a/src/main/java/aquality/selenium/core/visualization/VisualStateProvider.java +++ b/src/main/java/aquality/selenium/core/visualization/VisualStateProvider.java @@ -9,7 +9,6 @@ import java.util.function.Supplier; public class VisualStateProvider implements IVisualStateProvider { - private final IImageComparator imageComparator; private final IElementActionRetrier actionRetrier; private final Supplier getElement; @@ -22,6 +21,7 @@ public VisualStateProvider(IImageComparator imageComparator, IElementActionRetri this.stateLogger = stateLogger; } + @Override public Dimension getSize() { return getLoggedValue("size", element -> { final org.openqa.selenium.Dimension size = element.getSize(); @@ -29,6 +29,7 @@ public Dimension getSize() { }, null); } + @Override public Point getLocation() { return getLoggedValue("location", element -> { final org.openqa.selenium.Point location = element.getLocation(); @@ -36,6 +37,7 @@ public Point getLocation() { }, null); } + @Override public Image getImage() { return getLoggedValue("image", ImageFunctions::getScreenshotAsImage, image -> getStringValue(ImageFunctions.getSize(image))); From 4ac6fafd46e487dd6c6a044db421785650a3eef1 Mon Sep 17 00:00:00 2001 From: Aleksey2 Meleshko Date: Fri, 10 Mar 2023 23:57:03 +0100 Subject: [PATCH 08/12] Implemented DumpsManager and Form abstraction Finished visualization implementation, add tests --- .gitignore | 3 + .../core/applications/AqualityModule.java | 2 +- .../configurations/IConfigurationsModule.java | 6 +- ....java => IVisualizationConfiguration.java} | 2 +- ...n.java => VisualizationConfiguration.java} | 16 +- .../aquality/selenium/core/forms/Form.java | 160 ++++++++++++++ .../aquality/selenium/core/forms/IForm.java | 24 +++ .../core/visualization/DumpManager.java | 172 +++++++++++++++ .../core/visualization/IDumpManager.java | 43 ++++ .../visualization/IVisualStateProvider.java | 7 +- .../core/visualization/ImageComparator.java | 6 +- .../core/visualization/ImageFunctions.java | 56 +++-- .../visualization/VisualStateProvider.java | 3 +- src/main/resources/settings.json | 2 +- .../java/tests/elements/factory/Label.java | 15 ++ .../tests/visualization/FormDumpTests.java | 200 ++++++++++++++++++ src/test/java/theinternet/HoversForm.java | 101 +++++++++ .../java/theinternet/TheInternetPage.java | 3 +- 18 files changed, 777 insertions(+), 44 deletions(-) rename src/main/java/aquality/selenium/core/configurations/{IVisualConfiguration.java => IVisualizationConfiguration.java} (95%) rename src/main/java/aquality/selenium/core/configurations/{VisualConfiguration.java => VisualizationConfiguration.java} (76%) create mode 100644 src/main/java/aquality/selenium/core/forms/Form.java create mode 100644 src/main/java/aquality/selenium/core/forms/IForm.java create mode 100644 src/main/java/aquality/selenium/core/visualization/DumpManager.java create mode 100644 src/main/java/aquality/selenium/core/visualization/IDumpManager.java create mode 100644 src/test/java/tests/elements/factory/Label.java create mode 100644 src/test/java/tests/visualization/FormDumpTests.java create mode 100644 src/test/java/theinternet/HoversForm.java diff --git a/.gitignore b/.gitignore index 1dfa84a..eb7419f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ # Log file *.log +# Visualization +visualDumps/ + # BlueJ files *.ctxt diff --git a/src/main/java/aquality/selenium/core/applications/AqualityModule.java b/src/main/java/aquality/selenium/core/applications/AqualityModule.java index f547c38..f1ad21f 100644 --- a/src/main/java/aquality/selenium/core/applications/AqualityModule.java +++ b/src/main/java/aquality/selenium/core/applications/AqualityModule.java @@ -45,7 +45,7 @@ protected void configure() { bind(ITimeoutConfiguration.class).to(getTimeoutConfigurationImplementation()).in(Singleton.class); bind(IRetryConfiguration.class).to(getRetryConfigurationImplementation()).in(Singleton.class); bind(IElementCacheConfiguration.class).to(getElementCacheConfigurationImplementation()).in(Singleton.class); - bind(IVisualConfiguration.class).to(getVisualConfigurationImplementation()).in(Singleton.class); + bind(IVisualizationConfiguration.class).to(getVisualConfigurationImplementation()).in(Singleton.class); bind(IElementActionRetrier.class).to(getElementActionRetrierImplementation()).in(Singleton.class); bind(IActionRetrier.class).to(getActionRetrierImplementation()).in(Singleton.class); bind(ILocalizationManager.class).to(getLocalizationManagerImplementation()).in(Singleton.class); diff --git a/src/main/java/aquality/selenium/core/configurations/IConfigurationsModule.java b/src/main/java/aquality/selenium/core/configurations/IConfigurationsModule.java index 0fddf83..db6628f 100644 --- a/src/main/java/aquality/selenium/core/configurations/IConfigurationsModule.java +++ b/src/main/java/aquality/selenium/core/configurations/IConfigurationsModule.java @@ -5,10 +5,10 @@ */ public interface IConfigurationsModule { /** - * @return class which implements {@link IVisualConfiguration} + * @return class which implements {@link IVisualizationConfiguration} */ - default Class getVisualConfigurationImplementation() { - return VisualConfiguration.class; + default Class getVisualConfigurationImplementation() { + return VisualizationConfiguration.class; } /** diff --git a/src/main/java/aquality/selenium/core/configurations/IVisualConfiguration.java b/src/main/java/aquality/selenium/core/configurations/IVisualizationConfiguration.java similarity index 95% rename from src/main/java/aquality/selenium/core/configurations/IVisualConfiguration.java rename to src/main/java/aquality/selenium/core/configurations/IVisualizationConfiguration.java index c2941fb..74ffa8b 100644 --- a/src/main/java/aquality/selenium/core/configurations/IVisualConfiguration.java +++ b/src/main/java/aquality/selenium/core/configurations/IVisualizationConfiguration.java @@ -3,7 +3,7 @@ /** * Represents visualization configuration, used for image comparison. */ -public interface IVisualConfiguration { +public interface IVisualizationConfiguration { /** * Image format for comparison. * @return image format. diff --git a/src/main/java/aquality/selenium/core/configurations/VisualConfiguration.java b/src/main/java/aquality/selenium/core/configurations/VisualizationConfiguration.java similarity index 76% rename from src/main/java/aquality/selenium/core/configurations/VisualConfiguration.java rename to src/main/java/aquality/selenium/core/configurations/VisualizationConfiguration.java index dedce63..cd5c79b 100644 --- a/src/main/java/aquality/selenium/core/configurations/VisualConfiguration.java +++ b/src/main/java/aquality/selenium/core/configurations/VisualizationConfiguration.java @@ -4,16 +4,14 @@ import aquality.selenium.core.utilities.ISettingsFile; import com.google.inject.Inject; -import javax.imageio.ImageIO; import java.io.File; import java.io.IOException; -import java.util.Arrays; /** * Represents visualization configuration, used for image comparison. * Uses {@link ISettingsFile} as source for configuration values. */ -public class VisualConfiguration implements IVisualConfiguration { +public class VisualizationConfiguration implements IVisualizationConfiguration { private String imageFormat; private Integer maxFullFileNameLength; private Float defaultThreshold; @@ -28,21 +26,15 @@ public class VisualConfiguration implements IVisualConfiguration { * @param settingsFile settings file. */ @Inject - public VisualConfiguration(ISettingsFile settingsFile) { + public VisualizationConfiguration(ISettingsFile settingsFile) { this.settingsFile = settingsFile; } @Override public String getImageFormat() { if (imageFormat == null) { - String[] supportedFormats = ImageIO.getWriterFormatNames(); String valueFromConfig = settingsFile.getValueOrDefault("/visualization/imageExtension", "png").toString(); - String actualFormat = valueFromConfig.startsWith(".") ? valueFromConfig.substring(1) : valueFromConfig; - if (Arrays.stream(supportedFormats).noneMatch(format -> format.equals(actualFormat))) { - throw new IllegalArgumentException(String.format( - "Format [%s] is not supported by current JRE. Supported formats: %s", actualFormat, Arrays.toString(supportedFormats))); - } - imageFormat = actualFormat; + imageFormat = valueFromConfig.startsWith(".") ? valueFromConfig.substring(1) : valueFromConfig; } return imageFormat; } @@ -86,7 +78,7 @@ public int getComparisonHeight() { @Override public String getPathToDumps() { if (pathToDumps == null) { - pathToDumps = settingsFile.getValueOrDefault(".visualization.pathToDumps", "./src/test/resources/VisualDumps/").toString(); + pathToDumps = settingsFile.getValueOrDefault("/visualization/pathToDumps", "./src/test/resources/visualDumps/").toString(); if (pathToDumps.startsWith(".")) { try { pathToDumps = new File(pathToDumps).getCanonicalPath(); diff --git a/src/main/java/aquality/selenium/core/forms/Form.java b/src/main/java/aquality/selenium/core/forms/Form.java new file mode 100644 index 0000000..107b325 --- /dev/null +++ b/src/main/java/aquality/selenium/core/forms/Form.java @@ -0,0 +1,160 @@ +package aquality.selenium.core.forms; + +import aquality.selenium.core.configurations.IVisualizationConfiguration; +import aquality.selenium.core.elements.Element; +import aquality.selenium.core.elements.ElementState; +import aquality.selenium.core.elements.interfaces.IElement; +import aquality.selenium.core.localization.ILocalizedLogger; +import aquality.selenium.core.logging.Logger; +import aquality.selenium.core.visualization.DumpManager; +import aquality.selenium.core.visualization.IDumpManager; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Describes form that could be used for visualization purposes (see {@link IDumpManager}). + * + * @param Base type(class or interface) of elements of this form. + */ +public abstract class Form implements IForm { + private final Class elementClass; + + /** + * Initializes the form. + * + * @param elementClass Base type(class or interface) of elements of this form. + */ + protected Form(Class elementClass) { + this.elementClass = elementClass; + } + + /** + * Visualization configuration used by {@link IDumpManager}. + * Could be obtained from AqualityServices. + * + * @return visualization configuration. + */ + protected abstract IVisualizationConfiguration getVisualizationConfiguration(); + + /** + * Localized logger used by {@link IDumpManager}. + * Could be obtained from AqualityServices. + * + * @return localized logger. + */ + protected abstract ILocalizedLogger getLocalizedLogger(); + + /** + * Name of the current form. + * + * @return form's name. + */ + @Override + public abstract String getName(); + + /** + * Gets dump manager for the current form that could be used for visualization purposes, + * such as saving and comparing dumps. + * Uses getElementsForVisualization() as basis for dump creation and comparison. + * + * @return form's dump manager. + */ + @Override + public IDumpManager dump() { + return new DumpManager<>(getElementsForVisualization(), getName(), getVisualizationConfiguration(), getLocalizedLogger()); + } + + /** + * List of pairs uniqueName-element to be used for dump saving and comparing. + * By default, only currently displayed elements to be used with getDisplayedElements(). + * You can override this property with defined getAllElements(), getAllCurrentFormElements, + * getElementsInitializedAsDisplayed(), or your own element set. + * + * @return elements for visualization. + */ + protected Map getElementsForVisualization() { + return getDisplayedElements(); + } + + /** + * List of pairs uniqueName-element from the current form and it's parent forms, which are currently displayed. + * + * @return list of displayed elements. + */ + protected Map getDisplayedElements() { + return getAllElements().entrySet().stream().filter(element -> element.getValue().state().isDisplayed()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + /** + * List of pairs uniqueName-element from the current form and it's parent forms, which were initialized as displayed. + * + * @return list of displayed elements. + */ + protected Map getElementsInitializedAsDisplayed() { + return getAllElements().entrySet().stream().filter(element -> { + try { + Field stateField = Element.class.getDeclaredField("elementState"); + stateField.setAccessible(true); + ElementState elementState = (ElementState) stateField.get(element.getValue()); + stateField.setAccessible(false); + return ElementState.DISPLAYED == elementState; + } catch (ReflectiveOperationException exception) { + Logger.getInstance().fatal("Failed to read state the element: " + element.getKey(), exception); + return false; + } + }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + /** + * List of pairs uniqueName-element from the current form and it's parent forms. + * + * @return list of elements. + */ + protected Map getAllElements() { + Map map = new HashMap<>(); + addElementsToMap(map, this.getClass().getDeclaredFields()); + Class superClass = this.getClass().getSuperclass(); + while (superClass != null) { + addElementsToMap(map, superClass.getDeclaredFields()); + superClass = superClass.getSuperclass(); + } + return map; + } + + /** + * List of pairs uniqueName-element from the current form. + * + * @return list of elements. + */ + protected Map getAllCurrentFormElements() { + Map map = new HashMap<>(); + addElementsToMap(map, this.getClass().getDeclaredFields()); + return map; + } + + /** + * Adds pairs uniqueName-element from the specified fields array to map using the reflection. + * + * @param map map to save elements. + * @param fields any class fields (only assignable from target type will be added to map). + */ + protected void addElementsToMap(Map map, Field[] fields) { + for (Field field : fields) { + try { + if (elementClass.isAssignableFrom(field.getType())) { + field.setAccessible(true); + //noinspection unchecked + map.put(field.getName(), (T) field.get(this)); + field.setAccessible(false); + } + } + catch (IllegalAccessException exception) { + Logger.getInstance().fatal("Failed to read the element: " + field.getName(), exception); + } + } + } +} diff --git a/src/main/java/aquality/selenium/core/forms/IForm.java b/src/main/java/aquality/selenium/core/forms/IForm.java new file mode 100644 index 0000000..0ed4a80 --- /dev/null +++ b/src/main/java/aquality/selenium/core/forms/IForm.java @@ -0,0 +1,24 @@ +package aquality.selenium.core.forms; + +import aquality.selenium.core.visualization.IDumpManager; + +/** + * Describes form that could be used for visualization purposes, such as saving and comparing dumps. + * See {@link aquality.selenium.core.visualization.IDumpManager} for more details. + */ +public interface IForm { + /** + * Name of the current form. + * + * @return form's name. + */ + String getName(); + + /** + * Gets dump manager for the current form that could be used for visualization purposes, + * such as saving and comparing dumps. + * + * @return form's dump manager. + */ + IDumpManager dump(); +} diff --git a/src/main/java/aquality/selenium/core/visualization/DumpManager.java b/src/main/java/aquality/selenium/core/visualization/DumpManager.java new file mode 100644 index 0000000..f08ce7a --- /dev/null +++ b/src/main/java/aquality/selenium/core/visualization/DumpManager.java @@ -0,0 +1,172 @@ +package aquality.selenium.core.visualization; + +import aquality.selenium.core.configurations.IVisualizationConfiguration; +import aquality.selenium.core.elements.interfaces.IElement; +import aquality.selenium.core.localization.ILocalizedLogger; + +import java.io.File; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class DumpManager implements IDumpManager { + private static final String INVALID_CHARS_REGEX = "[\\\\/|\\\\\\\\|\\\\*|\\\\:|\\\\||\\\"|\\'|\\\\<|\\\\>|\\\\{|\\\\}|\\\\?|\\\\%|,]"; + + private final Map elementsForVisualization; + private final String formName; + private final ILocalizedLogger localizedLogger; + private final String imageFormat; + private final String imageExtension; + private final int maxFullFileNameLength; + private final String dumpsDirectory; + + public DumpManager(Map elementsForVisualization, String formName, IVisualizationConfiguration visualConfiguration, ILocalizedLogger localizedLogger) { + this.elementsForVisualization = elementsForVisualization; + this.formName = formName; + this.localizedLogger = localizedLogger; + this.imageFormat = visualConfiguration.getImageFormat(); + ImageFunctions.validateImageFormat(this.imageFormat); + this.imageExtension = String.format(".%s", imageFormat); + this.maxFullFileNameLength = visualConfiguration.getMaxFullFileNameLength(); + this.dumpsDirectory = visualConfiguration.getPathToDumps(); + } + + @Override + public float compare(String dumpName) { + File directory = getDumpDirectory(dumpName); + localizedLogger.info("loc.form.dump.compare", directory.getName()); + File[] imageFiles = getImageFiles(directory); + Map existingElements = filterElementsForVisualization().stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + int countOfUnprocessedElements = existingElements.size(); + int countOfProceededElements = 0; + float comparisonResult = 0f; + List absentOnFormElementNames = new ArrayList<>(); + for (File imageFile : imageFiles) { + String key = imageFile.getName().replace(imageExtension, ""); + if (!existingElements.containsKey(key)) { + localizedLogger.warn("loc.form.dump.elementnotfound", key); + countOfUnprocessedElements++; + absentOnFormElementNames.add(key); + } + else { + comparisonResult += existingElements.get(key).visual().getDifference(ImageFunctions.readImage(imageFile)); + countOfUnprocessedElements--; + countOfProceededElements++; + existingElements.remove(key); + } + } + logUnprocessedElements(countOfUnprocessedElements, existingElements, absentOnFormElementNames); + // adding of countOfUnprocessedElements means 100% difference for each element absent in dump or on page + float result = (comparisonResult + countOfUnprocessedElements) / (countOfProceededElements + countOfUnprocessedElements); + localizedLogger.info("loc.form.dump.compare.result", result * 100); + return result; + } + + private void logUnprocessedElements(int countOfUnprocessedElements, Map existingElements, List absentOnFormElementNames) { + if (countOfUnprocessedElements > 0) + { + if (existingElements.size() > 0) + { + localizedLogger.warn("loc.form.dump.elementsmissedindump", String.join(", ", existingElements.keySet())); + } + if (absentOnFormElementNames.size() > 0) + { + localizedLogger.warn("loc.form.dump.elementsmissedonform", String.join(", ", absentOnFormElementNames)); + } + localizedLogger.warn("loc.form.dump.unprocessedelements", countOfUnprocessedElements); + } + } + + private File[] getImageFiles(File directory) { + if (!directory.exists()) { + throw new IllegalArgumentException(String.format("Dump directory [%s] does not exist.", + directory.getAbsolutePath())); + } + File[] imageFiles = directory.listFiles((dir, name) -> name.endsWith(imageExtension)); + if (imageFiles == null || imageFiles.length == 0) { + throw new IllegalArgumentException(String.format("Dump directory [%s] does not contain any [*%s] files..", + directory.getAbsolutePath(), imageExtension)); + } + return imageFiles; + } + + @Override + public void save(String dumpName) { + File directory = cleanUpAndGetDumpDirectory(dumpName); + localizedLogger.info("loc.form.dump.save", directory.getName()); + filterElementsForVisualization() + .forEach(element -> { + try { + File file = Paths.get(directory.getAbsolutePath(), element.getKey() + imageExtension).toFile(); + ImageFunctions.save(element.getValue().visual().getImage(), file, imageFormat); + } catch (Exception e) { + localizedLogger.fatal("loc.form.dump.imagenotsaved", e, element.getKey(), e.getMessage()); + } + }); + } + + protected List> filterElementsForVisualization() { + return elementsForVisualization.entrySet().stream() + .filter(element -> element.getValue().state().isDisplayed()) + .collect(Collectors.toList()); + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + protected File cleanUpAndGetDumpDirectory(String dumpName) { + File directory = getDumpDirectory(dumpName); + if (directory.exists()) { + File[] dirFiles = directory.listFiles(); + if (dirFiles != null) { + Arrays.stream(dirFiles).filter(file -> !file.isDirectory()).forEach(File::delete); + } + } + else { + File dumpsDir = new File(dumpsDirectory); + if (!dumpsDir.exists()) { + dumpsDir.mkdir(); + } + directory.mkdirs(); + } + return directory; + } + + protected int getMaxNameLengthOfDumpElements() { + return elementsForVisualization.keySet().stream() + .mapToInt(elementName -> elementName == null ? 0 : elementName.length()) + .max().orElse(0); + } + + protected File getDumpDirectory(String dumpName) { + // get the maximum length of the name among the form elements for the dump + int maxNameLengthOfDumpElements = getMaxNameLengthOfDumpElements() + imageExtension.length(); + // get array of sub-folders in dump name + String[] dumpSubFoldersNames = (dumpName == null ? formName : dumpName).split(Pattern.quote(File.separator)); + // create new dump name without invalid chars for each subfolder + StringBuilder validDumpNameBuilder = new StringBuilder(); + for (String folderName : dumpSubFoldersNames) { + String folderNameCopy = folderName.trim(); + folderNameCopy = folderNameCopy.replaceAll(INVALID_CHARS_REGEX, " "); + if (folderNameCopy.endsWith(".")) { + folderNameCopy = folderNameCopy.substring(0, folderNameCopy.length() - 1); + } + validDumpNameBuilder.append(folderNameCopy).append(File.separator); + } + String validDumpNameString = validDumpNameBuilder.toString(); + // create full dump path + File fullDumpPath = Paths.get(dumpsDirectory, validDumpNameString).toFile(); + // cut off the excess length and log warn message + if (fullDumpPath.getPath().length() + maxNameLengthOfDumpElements > maxFullFileNameLength) + { + validDumpNameString = validDumpNameString.substring(0, + maxFullFileNameLength - new File(dumpsDirectory).getAbsolutePath().length() - maxNameLengthOfDumpElements); + fullDumpPath = Paths.get(dumpsDirectory, validDumpNameString).toFile(); + localizedLogger.warn("loc.form.dump.exceededdumpname", fullDumpPath); + } + + return fullDumpPath; + } +} diff --git a/src/main/java/aquality/selenium/core/visualization/IDumpManager.java b/src/main/java/aquality/selenium/core/visualization/IDumpManager.java new file mode 100644 index 0000000..9c56842 --- /dev/null +++ b/src/main/java/aquality/selenium/core/visualization/IDumpManager.java @@ -0,0 +1,43 @@ +package aquality.selenium.core.visualization; + +/** + * Describes dump manager for the form that could be used for visualization purposes, such as saving and comparing dumps. + */ +public interface IDumpManager { + /** + * Compares current form with the dump saved previously. + * + * @param dumpName custom name of the sub-folder where the dump was saved. + * @return The difference of comparing the page to the dump as a percentage (no difference is 0%). + * In the default implementation, it is calculated as sum of element differences divided by elements count. + */ + float compare(String dumpName); + + /** + * Compares current form with the dump saved previously. + * Form name is used by default for the name of the sub-folder where the dump was saved. + * + * @return The difference of comparing the page to the dump as a percentage (no difference is 0%). + * In the default implementation, it is calculated as sum of element differences divided by elements count. + */ + default float compare() { + return compare(null); + } + + /** + * Saves the dump of the current form. + * In the default implementation, it is a set of screenshots of selected form elements. + * + * @param dumpName Name of the sub-folder where to save the dump. + */ + void save(String dumpName); + + /** + * Saves the dump of the current form. + * In the default implementation, it is a set of screenshots of selected form elements. + * Form name is used by default for the name of the sub-folder where to save the dump. + */ + default void save() { + save(null); + } +} diff --git a/src/main/java/aquality/selenium/core/visualization/IVisualStateProvider.java b/src/main/java/aquality/selenium/core/visualization/IVisualStateProvider.java index 957910c..3fa7418 100644 --- a/src/main/java/aquality/selenium/core/visualization/IVisualStateProvider.java +++ b/src/main/java/aquality/selenium/core/visualization/IVisualStateProvider.java @@ -1,6 +1,9 @@ package aquality.selenium.core.visualization; +import aquality.selenium.core.configurations.IVisualizationConfiguration; + import java.awt.*; +import java.awt.image.BufferedImage; /** * Provides visual state of the element. @@ -24,7 +27,7 @@ public interface IVisualStateProvider { * Gets an image containing the screenshot of the element. * @return screenshot of the element. */ - Image getImage(); + BufferedImage getImage(); /** * Gets the difference between the image of the element and the provided image using {@link IImageComparator}. @@ -36,7 +39,7 @@ public interface IVisualStateProvider { /** * Gets the difference between the image of the element and the provided image using {@link IImageComparator}. - * The threshold value is got from {@link aquality.selenium.core.configurations.IVisualConfiguration}. + * The threshold value is got from {@link IVisualizationConfiguration}. * @param theOtherOne the image to compare the element with. * @return the difference between the two images as a percentage - value between 0 and 1. */ diff --git a/src/main/java/aquality/selenium/core/visualization/ImageComparator.java b/src/main/java/aquality/selenium/core/visualization/ImageComparator.java index cc36a8b..10e4836 100644 --- a/src/main/java/aquality/selenium/core/visualization/ImageComparator.java +++ b/src/main/java/aquality/selenium/core/visualization/ImageComparator.java @@ -1,6 +1,6 @@ package aquality.selenium.core.visualization; -import aquality.selenium.core.configurations.IVisualConfiguration; +import aquality.selenium.core.configurations.IVisualizationConfiguration; import com.google.inject.Inject; import java.awt.*; @@ -8,10 +8,10 @@ public class ImageComparator implements IImageComparator { private static final int THRESHOLD_DIVISOR = 255; - private final IVisualConfiguration visualConfiguration; + private final IVisualizationConfiguration visualConfiguration; @Inject - public ImageComparator(IVisualConfiguration visualConfiguration) { + public ImageComparator(IVisualizationConfiguration visualConfiguration) { this.visualConfiguration = visualConfiguration; } diff --git a/src/main/java/aquality/selenium/core/visualization/ImageFunctions.java b/src/main/java/aquality/selenium/core/visualization/ImageFunctions.java index 4108126..ee53f3d 100644 --- a/src/main/java/aquality/selenium/core/visualization/ImageFunctions.java +++ b/src/main/java/aquality/selenium/core/visualization/ImageFunctions.java @@ -5,15 +5,12 @@ import org.openqa.selenium.OutputType; import org.openqa.selenium.remote.RemoteWebElement; -import javax.imageio.IIOImage; import javax.imageio.ImageIO; -import javax.imageio.ImageWriteParam; -import javax.imageio.ImageWriter; -import javax.imageio.stream.FileImageInputStream; import java.awt.*; import java.awt.image.BufferedImage; import java.awt.image.RenderedImage; import java.io.*; +import java.util.Arrays; import static java.awt.image.BufferedImage.TYPE_INT_RGB; @@ -25,6 +22,20 @@ private ImageFunctions() throws InstantiationException { throw new InstantiationException("Static ImageFunctions should not be initialized"); } + /** + * Reads image from file. + * @param file The file to read the image from. + * @return Instance of BufferedImage. + */ + public static BufferedImage readImage(File file) { + try { + return ImageIO.read(file); + } catch (IOException exception) { + Logger.getInstance().fatal(String.format("Failed to get the file [%s] as an image", file), exception); + throw new UncheckedIOException(exception); + } + } + /** * Represents given element's screenshot as an image. * @@ -106,24 +117,31 @@ public static BufferedImage resize(Image image, int width, int height) { } /** - * Saves image in the highest quality. + * Redraw the image on white background and saves it to target file. * @param image source image. - * @param name target name without extension. + * @param file target file. * @param format target format. */ - public static void save(RenderedImage image, String name, String format) { - final float highestQuality = 1.0F; - ImageWriter writer = ImageIO.getImageWritersByFormatName(format).next(); - ImageWriteParam param = writer.getDefaultWriteParam(); - param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); - param.setCompressionQuality(highestQuality); - String fileName = String.format("%s.%s", name, format.startsWith(".") ? format.substring(1) : format); - try { - writer.setOutput(new FileImageInputStream(new File(fileName))); - writer.write(null, new IIOImage(image, null, null), param); - } catch (IOException e) { - Logger.getInstance().fatal("Failed to save the image: " + e.getMessage(), e); - throw new UncheckedIOException(e); + public static void save(Image image, File file, String format) throws IOException { + if (file.exists() || file.createNewFile()) { + BufferedImage newImage = new BufferedImage(image.getWidth(null), image.getHeight(null), TYPE_INT_RGB); + Graphics2D graphics = newImage.createGraphics(); + graphics.drawImage(image, 0, 0, Color.WHITE, null); + graphics.dispose(); + ImageIO.write(newImage, format, file); + } + } + + /** + * Validates that image format is supported by the current JRE. + * + * @param actualFormat image format to check. + */ + public static void validateImageFormat(String actualFormat) { + String[] supportedFormats = ImageIO.getWriterFormatNames(); + if (Arrays.stream(supportedFormats).noneMatch(format -> format.equals(actualFormat))) { + throw new IllegalArgumentException(String.format( + "Format [%s] is not supported by current JRE. Supported formats: %s", actualFormat, Arrays.toString(supportedFormats))); } } } diff --git a/src/main/java/aquality/selenium/core/visualization/VisualStateProvider.java b/src/main/java/aquality/selenium/core/visualization/VisualStateProvider.java index 4b1bf72..f076926 100644 --- a/src/main/java/aquality/selenium/core/visualization/VisualStateProvider.java +++ b/src/main/java/aquality/selenium/core/visualization/VisualStateProvider.java @@ -5,6 +5,7 @@ import org.openqa.selenium.remote.RemoteWebElement; import java.awt.*; +import java.awt.image.BufferedImage; import java.util.function.Function; import java.util.function.Supplier; @@ -38,7 +39,7 @@ public Point getLocation() { } @Override - public Image getImage() { + public BufferedImage getImage() { return getLoggedValue("image", ImageFunctions::getScreenshotAsImage, image -> getStringValue(ImageFunctions.getSize(image))); } diff --git a/src/main/resources/settings.json b/src/main/resources/settings.json index a3c86bc..d39620c 100644 --- a/src/main/resources/settings.json +++ b/src/main/resources/settings.json @@ -22,6 +22,6 @@ "defaultThreshold": 0.012, "comparisonWidth": 16, "comparisonHeight": 16, - "pathToDumps": "../../../Resources/VisualDumps/" + "pathToDumps": "./src/test/resources/visualDumps/" } } diff --git a/src/test/java/tests/elements/factory/Label.java b/src/test/java/tests/elements/factory/Label.java new file mode 100644 index 0000000..34aaf0f --- /dev/null +++ b/src/test/java/tests/elements/factory/Label.java @@ -0,0 +1,15 @@ +package tests.elements.factory; + +import aquality.selenium.core.elements.ElementState; +import org.openqa.selenium.By; + +public class Label extends CustomWebElement { + public Label(By locator, String name, ElementState state) { + super(locator, name, state); + } + + @Override + protected String getElementType() { + return "Label"; + } +} diff --git a/src/test/java/tests/visualization/FormDumpTests.java b/src/test/java/tests/visualization/FormDumpTests.java new file mode 100644 index 0000000..a1f947b --- /dev/null +++ b/src/test/java/tests/visualization/FormDumpTests.java @@ -0,0 +1,200 @@ +package tests.visualization; + +import aquality.selenium.core.configurations.IVisualizationConfiguration; +import aquality.selenium.core.configurations.VisualizationConfiguration; +import aquality.selenium.core.utilities.ISettingsFile; +import aquality.selenium.core.visualization.DumpManager; +import org.apache.commons.lang3.StringUtils; +import org.testng.Assert; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import tests.applications.browser.AqualityServices; +import tests.applications.browser.ITheInternetPageTest; +import theinternet.HoversForm; +import theinternet.TheInternetPage; + +import javax.imageio.ImageIO; +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.file.Paths; +import java.util.Arrays; + +public class FormDumpTests implements ITheInternetPageTest { + private String getPathToDumps() { + return AqualityServices.get(IVisualizationConfiguration.class).getPathToDumps(); + } + + @Override + @BeforeMethod + public void beforeMethod() { + navigate(TheInternetPage.HOVERS); + } + + @Test + public void shouldBePossibleToSaveFormDumpWithDefaultName() + { + HoversForm form = new HoversForm(); + File pathToDump = cleanUpAndGetPathToDump(form.getName().replace("/", " ")); + form.dump().save(); + Assert.assertTrue(pathToDump.exists() && pathToDump.isDirectory()); + File[] files = pathToDump.listFiles(); + Assert.assertTrue(files != null && files.length > 0, "Dump should contain some files"); + } + + @Test + public void shouldBePossibleToSaveFormDumpWithSubFoldersInName() + { + HoversForm form = new HoversForm(); + String dumpName = String.format("SubFolder1%sSubFolder2", File.separatorChar); + File pathToDump = cleanUpAndGetPathToDump(dumpName); + form.dump().save(dumpName); + File[] files = pathToDump.listFiles(); + Assert.assertTrue(files != null && files.length > 0, "Dump should contain some files"); + } + + @Test + public void shouldBePossibleToCompareWithDumpWithCustomNameWhenDifferenceIsZero() + { + HoversForm form = new HoversForm(); + form.dump().save("Zero diff"); + Assert.assertEquals(form.dump().compare("Zero diff"), 0, "Difference with current page should be around zero"); + } + + @Test + public void shouldBePossibleToCompareWithDumpWithCustomNameWhenDifferenceIsNotZero() + { + HoversForm form = new HoversForm(); + form.hoverAvatar(); + form.dump().save("Non-zero diff"); + AqualityServices.getApplication().getDriver().navigate().refresh(); + form.waitUntilPresent(); + Assert.assertTrue(form.dump().compare("Non-zero diff") > 0, "Difference with current page should be greater than zero"); + } + + @Test + public void shouldBePossibleToCompareWithDumpWithCustomNameWhenElementSetDiffers() + { + HoversForm customForm = new HoversForm(); + customForm.dump().save("Set differs"); + customForm.setElementsForDump(HoversForm.ElementsFilter.INITIALIZED_AS_DISPLAYED); + Assert.assertTrue(customForm.dump().compare("Set differs") > 0, "Difference with current page should be greater than zero if element set differs"); + } + + @Test + public void shouldBePossibleToSaveAndCompareWithDumpWithCustomNameWhenAllElementsSelected() + { + HoversForm customForm = new HoversForm(); + customForm.setElementsForDump(HoversForm.ElementsFilter.ALL_ELEMENTS); + customForm.dump().save("All elements"); + Assert.assertEquals(customForm.dump().compare("All elements"), 0, "Some elements should be failed to take image, but difference should be around zero"); + } + + @Test + public void shouldBePossibleToSaveAndCompareWithDumpWithOverLengthDumpNameWhenAllElementsSelected() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException { + HoversForm customForm = new HoversForm(); + customForm.setElementsForDump(HoversForm.ElementsFilter.ALL_ELEMENTS); + + final Method getMaxNameLengthOfDumpElements = DumpManager.class.getDeclaredMethod("getMaxNameLengthOfDumpElements"); + getMaxNameLengthOfDumpElements.setAccessible(true); + int maxElementNameLength = (int) getMaxNameLengthOfDumpElements.invoke(customForm.dump()); + getMaxNameLengthOfDumpElements.setAccessible(false); + final Field imageExtension = DumpManager.class.getDeclaredField("imageExtension"); + imageExtension.setAccessible(true); + int imageExtensionLength = ((String) imageExtension.get(customForm.dump())).length(); + imageExtension.setAccessible(false); + final Field maxFullFileNameLength = DumpManager.class.getDeclaredField("maxFullFileNameLength"); + maxFullFileNameLength.setAccessible(true); + int maxLength = (int) maxFullFileNameLength.get(customForm.dump()); + maxFullFileNameLength.setAccessible(false); + int pathToDumpLength = getPathToDumps().length(); + + String dumpName = StringUtils.repeat('A', maxLength - pathToDumpLength - maxElementNameLength - imageExtensionLength); + String overLengthDumpName = dumpName + "_BCDE"; + + File overLengthPathToDump = cleanUpAndGetPathToDump(overLengthDumpName); + File pathToDump = cleanUpAndGetPathToDump(dumpName); + + customForm.dump().save(overLengthDumpName); + Assert.assertFalse(overLengthPathToDump.exists()); + Assert.assertTrue(pathToDump.exists()); + + Assert.assertEquals(customForm.dump().compare(dumpName), 0, "Some elements should be failed to take image, but difference should be around zero"); + } + + @DataProvider + private Object[] validFormats() { + return ImageIO.getWriterFormatNames(); + } + + @Test (dataProvider = "validFormats") + public void shouldBePossibleToSaveFormDumpWithValidExtension(String imageFormat) { + LiteWebForm form = new LiteWebForm(imageFormat); + String dumpName = String.format("Test .%s extension", imageFormat); + File pathToDump = cleanUpAndGetPathToDump(dumpName); + form.dump().save(dumpName); + Assert.assertTrue(pathToDump.exists()); + File[] files = pathToDump.listFiles(); + Assert.assertTrue(files != null && files.length > 0, "Dump should contain some files"); + + for (File file : files) { + String name = file.getName(); + Assert.assertEquals(name.substring(name.lastIndexOf(".") + 1), imageFormat); + } + } + + @Test + public void shouldBeImpossibleToSaveFormDumpWithInvalidExtension() + { + LiteWebForm form = new LiteWebForm("abc"); + String dumpName = "Test .abc extension"; + File pathToDump = cleanUpAndGetPathToDump(dumpName); + Assert.assertThrows(IllegalArgumentException.class, () -> form.dump().save(dumpName)); + File[] files = pathToDump.listFiles(); + Assert.assertTrue(files == null || files.length == 0, "No dump files should be saved"); + } + + private File cleanUpAndGetPathToDump(String dumpName) { + File pathToDump = Paths.get(getPathToDumps(), dumpName).toFile(); + File[] files = pathToDump.listFiles(); + if (pathToDump.exists() && files != null) { + Arrays.stream(files).forEach(File::delete); + } + files = pathToDump.listFiles(); + Assert.assertEquals(files == null ? 0 : files.length, 0, "Dump directory should not contain any files before saving"); + return pathToDump; + } + + private static class LiteWebForm extends HoversForm + { + private final String imageFormat; + + @Override + protected IVisualizationConfiguration getVisualizationConfiguration() { + return new CustomVisualizationConfiguration(imageFormat); + } + + public LiteWebForm(String imageFormat) + { + this.imageFormat = imageFormat; + } + + private class CustomVisualizationConfiguration extends VisualizationConfiguration + { + private final String imageFormat; + + @Override + public String getImageFormat() { + return imageFormat; + } + + public CustomVisualizationConfiguration(String format) + { + super(AqualityServices.get(ISettingsFile.class)); + imageFormat = format; + } + } + } +} diff --git a/src/test/java/theinternet/HoversForm.java b/src/test/java/theinternet/HoversForm.java new file mode 100644 index 0000000..9ef415b --- /dev/null +++ b/src/test/java/theinternet/HoversForm.java @@ -0,0 +1,101 @@ +package theinternet; + +import aquality.selenium.core.configurations.IVisualizationConfiguration; +import aquality.selenium.core.elements.ElementState; +import aquality.selenium.core.elements.interfaces.IElementFactory; +import aquality.selenium.core.forms.Form; +import aquality.selenium.core.localization.ILocalizedLogger; +import aquality.selenium.core.logging.Logger; +import org.openqa.selenium.By; +import org.openqa.selenium.interactions.Actions; +import tests.applications.browser.AqualityServices; +import tests.elements.factory.CustomWebElement; +import tests.elements.factory.Label; + +import java.util.Map; + +public class HoversForm extends Form { + private static final By ContentLoc = By.xpath("//div[contains(@class,'example')]"); + private static final By HiddenElementsLoc = By.xpath("//h5"); + private static final By NotExistingElementsLoc = By.xpath("//h5"); + private static final By DisplayedElementsLoc = By.xpath("//div[contains(@class,'figure')]"); + + private final Label displayedLabel = AqualityServices.get(IElementFactory.class).getCustomElement( + Label::new, DisplayedElementsLoc, "I'm displayed field"); + private final Label displayedButInitializedAsExist = new Label(DisplayedElementsLoc, "I'm displayed but initialized as existing", ElementState.EXISTS_IN_ANY_STATE); + private final Label notExistingButInitializedAsExist = new Label(HiddenElementsLoc, "I'm notExisting but initialized as existing", ElementState.EXISTS_IN_ANY_STATE); + protected final Label displayedProtectedLabel = new Label(DisplayedElementsLoc, "I'm displayed protected", ElementState.DISPLAYED); + private final Label hiddenLabel = new Label(HiddenElementsLoc, "I'm hidden", ElementState.EXISTS_IN_ANY_STATE); + private final Label hiddenLabelInitializedAsDisplayed = new Label(HiddenElementsLoc, "I'm hidden but mask as displayed", ElementState.DISPLAYED); + protected final Label contentLabel = new Label(ContentLoc, "Content", ElementState.DISPLAYED); + private final Label contentDuplicateLabel = new Label(ContentLoc, "Content", ElementState.DISPLAYED); + + private Map elementsToCheck; + + public HoversForm() { + super(CustomWebElement.class); + } + + public void clickOnContent() { + contentLabel.click(); + } + + public void waitUntilPresent() { + displayedLabel.state().waitForClickable(); + } + + public void hoverAvatar() { + Logger.getInstance().info("Hovering avatar"); + new Actions(AqualityServices.getApplication().getDriver()).moveToElement(displayedLabel.getElement()) + .clickAndHold().build().perform(); + hiddenLabel.state().waitForDisplayed(); + } + + @Override + protected IVisualizationConfiguration getVisualizationConfiguration() { + return AqualityServices.get(IVisualizationConfiguration.class); + } + + @Override + protected ILocalizedLogger getLocalizedLogger() { + return AqualityServices.get(ILocalizedLogger.class); + } + + @Override + public String getName() { + return "Hovers Web page/form"; + } + + @Override + protected Map getElementsForVisualization() { + if (elementsToCheck == null) { + elementsToCheck = super.getElementsForVisualization(); + } + return elementsToCheck; + } + + public void setElementsForDump(ElementsFilter filter) { + switch (filter) { + case INITIALIZED_AS_DISPLAYED: + elementsToCheck = getElementsInitializedAsDisplayed(); + break; + case DISPLAYED_ELEMENTS: + elementsToCheck = getDisplayedElements(); + break; + case CURRENT_FORM_ELEMENTS: + elementsToCheck = getAllCurrentFormElements(); + break; + case ALL_ELEMENTS: + default: + elementsToCheck = getAllElements(); + break; + } + } + + public enum ElementsFilter { + ALL_ELEMENTS, + DISPLAYED_ELEMENTS, + INITIALIZED_AS_DISPLAYED, + CURRENT_FORM_ELEMENTS + } +} diff --git a/src/test/java/theinternet/TheInternetPage.java b/src/test/java/theinternet/TheInternetPage.java index 3b7152e..d761a20 100644 --- a/src/test/java/theinternet/TheInternetPage.java +++ b/src/test/java/theinternet/TheInternetPage.java @@ -3,7 +3,8 @@ public enum TheInternetPage { DYNAMIC_CONTROLS, DYNAMIC_LOADING("dynamic_loading/1"), - INPUTS("inputs"); + HOVERS, + INPUTS; private static final String BASE_URL = "http://the-internet.herokuapp.com/"; From d6715423546c8597200be360ab990423fbb8c2de Mon Sep 17 00:00:00 2001 From: Aleksey2 Meleshko Date: Sat, 11 Mar 2023 00:03:11 +0100 Subject: [PATCH 09/12] Raise package version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3aa2af5..77c01a0 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.aquality-automation aquality-selenium-core - 2.0.6 + 3.0.0 jar Aquality Selenium Core From 1363404692aeb48cd24e1361d02df3c63ff63b7f Mon Sep 17 00:00:00 2001 From: Aleksey2 Meleshko Date: Sat, 11 Mar 2023 00:08:22 +0100 Subject: [PATCH 10/12] Use isEmpty() instead of size check --- .../aquality/selenium/core/visualization/DumpManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/aquality/selenium/core/visualization/DumpManager.java b/src/main/java/aquality/selenium/core/visualization/DumpManager.java index f08ce7a..369bcce 100644 --- a/src/main/java/aquality/selenium/core/visualization/DumpManager.java +++ b/src/main/java/aquality/selenium/core/visualization/DumpManager.java @@ -69,11 +69,11 @@ public float compare(String dumpName) { private void logUnprocessedElements(int countOfUnprocessedElements, Map existingElements, List absentOnFormElementNames) { if (countOfUnprocessedElements > 0) { - if (existingElements.size() > 0) + if (!existingElements.isEmpty()) { localizedLogger.warn("loc.form.dump.elementsmissedindump", String.join(", ", existingElements.keySet())); } - if (absentOnFormElementNames.size() > 0) + if (!absentOnFormElementNames.isEmpty()) { localizedLogger.warn("loc.form.dump.elementsmissedonform", String.join(", ", absentOnFormElementNames)); } From 8a66a1af76d2e60e5fb104ba65a653705d65ef16 Mon Sep 17 00:00:00 2001 From: Aleksey2 Meleshko Date: Mon, 13 Mar 2023 18:11:43 +0100 Subject: [PATCH 11/12] Remove background color overriding --- .../aquality/selenium/core/visualization/ImageFunctions.java | 4 ++-- .../java/tests/applications/browser/ChromeApplication.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/aquality/selenium/core/visualization/ImageFunctions.java b/src/main/java/aquality/selenium/core/visualization/ImageFunctions.java index ee53f3d..c4a46c5 100644 --- a/src/main/java/aquality/selenium/core/visualization/ImageFunctions.java +++ b/src/main/java/aquality/selenium/core/visualization/ImageFunctions.java @@ -117,7 +117,7 @@ public static BufferedImage resize(Image image, int width, int height) { } /** - * Redraw the image on white background and saves it to target file. + * Redraw the image and saves it to target file. * @param image source image. * @param file target file. * @param format target format. @@ -126,7 +126,7 @@ public static void save(Image image, File file, String format) throws IOExceptio if (file.exists() || file.createNewFile()) { BufferedImage newImage = new BufferedImage(image.getWidth(null), image.getHeight(null), TYPE_INT_RGB); Graphics2D graphics = newImage.createGraphics(); - graphics.drawImage(image, 0, 0, Color.WHITE, null); + graphics.drawImage(image, 0, 0, null); graphics.dispose(); ImageIO.write(newImage, format, file); } diff --git a/src/test/java/tests/applications/browser/ChromeApplication.java b/src/test/java/tests/applications/browser/ChromeApplication.java index 3305a37..8e078d0 100644 --- a/src/test/java/tests/applications/browser/ChromeApplication.java +++ b/src/test/java/tests/applications/browser/ChromeApplication.java @@ -12,7 +12,7 @@ public class ChromeApplication implements IApplication { private final RemoteWebDriver driver; ChromeApplication(long implicitWaitSeconds) { - driver = new ChromeDriver(new ChromeOptions().addArguments("--headless")); + driver = new ChromeDriver(new ChromeOptions().addArguments("--headless").addArguments("--remote-allow-origins=*")); setImplicitWaitTimeout(Duration.ofSeconds(implicitWaitSeconds)); } From 53983fa0153a579a17b6a9254fd81418b826c7f3 Mon Sep 17 00:00:00 2001 From: Aleksey2 Meleshko Date: Tue, 14 Mar 2023 14:33:17 +0100 Subject: [PATCH 12/12] Update invalid chars regex --- .../java/aquality/selenium/core/visualization/DumpManager.java | 2 +- src/test/java/tests/applications/browser/ChromeApplication.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/aquality/selenium/core/visualization/DumpManager.java b/src/main/java/aquality/selenium/core/visualization/DumpManager.java index 369bcce..83d4c28 100644 --- a/src/main/java/aquality/selenium/core/visualization/DumpManager.java +++ b/src/main/java/aquality/selenium/core/visualization/DumpManager.java @@ -14,7 +14,7 @@ import java.util.stream.Collectors; public class DumpManager implements IDumpManager { - private static final String INVALID_CHARS_REGEX = "[\\\\/|\\\\\\\\|\\\\*|\\\\:|\\\\||\\\"|\\'|\\\\<|\\\\>|\\\\{|\\\\}|\\\\?|\\\\%|,]"; + private static final String INVALID_CHARS_REGEX = "[\\\\/|*:\"'<>{}?%,]"; private final Map elementsForVisualization; private final String formName; diff --git a/src/test/java/tests/applications/browser/ChromeApplication.java b/src/test/java/tests/applications/browser/ChromeApplication.java index 8e078d0..5ab87be 100644 --- a/src/test/java/tests/applications/browser/ChromeApplication.java +++ b/src/test/java/tests/applications/browser/ChromeApplication.java @@ -12,7 +12,7 @@ public class ChromeApplication implements IApplication { private final RemoteWebDriver driver; ChromeApplication(long implicitWaitSeconds) { - driver = new ChromeDriver(new ChromeOptions().addArguments("--headless").addArguments("--remote-allow-origins=*")); + driver = new ChromeDriver(new ChromeOptions().addArguments("--headless", "--remote-allow-origins=*")); setImplicitWaitTimeout(Duration.ofSeconds(implicitWaitSeconds)); }