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/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
diff --git a/src/main/java/aquality/selenium/core/applications/AqualityModule.java b/src/main/java/aquality/selenium/core/applications/AqualityModule.java
index 8411a45..f1ad21f 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;
@@ -42,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(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);
@@ -49,5 +53,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/configurations/IConfigurationsModule.java b/src/main/java/aquality/selenium/core/configurations/IConfigurationsModule.java
index 980e728..db6628f 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 IVisualizationConfiguration}
+ */
+ default Class extends IVisualizationConfiguration> getVisualConfigurationImplementation() {
+ return VisualizationConfiguration.class;
+ }
+
/**
* @return class which implements {@link IElementCacheConfiguration}
*/
diff --git a/src/main/java/aquality/selenium/core/configurations/IVisualizationConfiguration.java b/src/main/java/aquality/selenium/core/configurations/IVisualizationConfiguration.java
new file mode 100644
index 0000000..74ffa8b
--- /dev/null
+++ b/src/main/java/aquality/selenium/core/configurations/IVisualizationConfiguration.java
@@ -0,0 +1,42 @@
+package aquality.selenium.core.configurations;
+
+/**
+ * Represents visualization configuration, used for image comparison.
+ */
+public interface IVisualizationConfiguration {
+ /**
+ * Image format for comparison.
+ * @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/VisualizationConfiguration.java b/src/main/java/aquality/selenium/core/configurations/VisualizationConfiguration.java
new file mode 100644
index 0000000..cd5c79b
--- /dev/null
+++ b/src/main/java/aquality/selenium/core/configurations/VisualizationConfiguration.java
@@ -0,0 +1,94 @@
+package aquality.selenium.core.configurations;
+
+import aquality.selenium.core.logging.Logger;
+import aquality.selenium.core.utilities.ISettingsFile;
+import com.google.inject.Inject;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Represents visualization configuration, used for image comparison.
+ * Uses {@link ISettingsFile} as source for configuration values.
+ */
+public class VisualizationConfiguration implements IVisualizationConfiguration {
+ private String imageFormat;
+ private Integer maxFullFileNameLength;
+ private Float defaultThreshold;
+ private Integer comparisonWidth;
+ private Integer comparisonHeight;
+ private String pathToDumps;
+
+ private final ISettingsFile settingsFile;
+
+ /**
+ * Instantiates class using {@link ISettingsFile} with visualization settings.
+ * @param settingsFile settings file.
+ */
+ @Inject
+ public VisualizationConfiguration(ISettingsFile settingsFile) {
+ this.settingsFile = settingsFile;
+ }
+
+ @Override
+ public String getImageFormat() {
+ if (imageFormat == null) {
+ String valueFromConfig = settingsFile.getValueOrDefault("/visualization/imageExtension", "png").toString();
+ imageFormat = valueFromConfig.startsWith(".") ? valueFromConfig.substring(1) : valueFromConfig;
+ }
+ 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;
+ }
+}
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/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/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/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/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/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/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/DumpManager.java b/src/main/java/aquality/selenium/core/visualization/DumpManager.java
new file mode 100644
index 0000000..83d4c28
--- /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.isEmpty())
+ {
+ localizedLogger.warn("loc.form.dump.elementsmissedindump", String.join(", ", existingElements.keySet()));
+ }
+ if (!absentOnFormElementNames.isEmpty())
+ {
+ 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/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/IVisualStateProvider.java b/src/main/java/aquality/selenium/core/visualization/IVisualStateProvider.java
new file mode 100644
index 0000000..3fa7418
--- /dev/null
+++ b/src/main/java/aquality/selenium/core/visualization/IVisualStateProvider.java
@@ -0,0 +1,47 @@
+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.
+ */
+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.
+ */
+ BufferedImage 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.
+ * @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 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.
+ */
+ float getDifference(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 extends IImageComparator> 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..10e4836
--- /dev/null
+++ b/src/main/java/aquality/selenium/core/visualization/ImageComparator.java
@@ -0,0 +1,85 @@
+package aquality.selenium.core.visualization;
+
+import aquality.selenium.core.configurations.IVisualizationConfiguration;
+import com.google.inject.Inject;
+
+import java.awt.*;
+import java.awt.image.BufferedImage;
+
+public class ImageComparator implements IImageComparator {
+ private static final int THRESHOLD_DIVISOR = 255;
+ private final IVisualizationConfiguration visualConfiguration;
+
+ @Inject
+ public ImageComparator(IVisualizationConfiguration visualConfiguration) {
+ this.visualConfiguration = visualConfiguration;
+ }
+
+ private int getComparisonHeight() {
+ return visualConfiguration.getComparisonHeight();
+ }
+
+ 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));
+ }
+
+ int intThreshold = (int) (threshold * THRESHOLD_DIVISOR);
+ return percentageDifference(thisOne, theOtherOne, intThreshold);
+ }
+
+ 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) (getComparisonWidth() * getComparisonHeight());
+ }
+
+ protected int[][] getDifferences(Image thisOne, Image theOtherOne) {
+ int[][] firstGray = getResizedGrayScaleValues(thisOne);
+ int[][] secondGray = getResizedGrayScaleValues(theOtherOne);
+
+ 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]);
+ }
+ }
+
+ return differences;
+ }
+
+ protected int[][] getResizedGrayScaleValues(Image image) {
+ 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);
+ }
+ }
+
+ return grayScale;
+ }
+}
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..c4a46c5
--- /dev/null
+++ b/src/main/java/aquality/selenium/core/visualization/ImageFunctions.java
@@ -0,0 +1,147 @@
+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.*;
+import java.util.Arrays;
+
+import static java.awt.image.BufferedImage.TYPE_INT_RGB;
+
+/**
+ * Image and screenshot extensions.
+ */
+public class ImageFunctions {
+ 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.
+ *
+ * @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));
+ }
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Redraw the image and saves it to target file.
+ * @param image source image.
+ * @param file target file.
+ * @param format target format.
+ */
+ 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, 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
new file mode 100644
index 0000000..f076926
--- /dev/null
+++ b/src/main/java/aquality/selenium/core/visualization/VisualStateProvider.java
@@ -0,0 +1,84 @@
+package aquality.selenium.core.visualization;
+
+import aquality.selenium.core.logging.ILogVisualState;
+import aquality.selenium.core.utilities.IElementActionRetrier;
+import org.openqa.selenium.remote.RemoteWebElement;
+
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+public class VisualStateProvider implements IVisualStateProvider {
+ private final IImageComparator imageComparator;
+ private final IElementActionRetrier actionRetrier;
+ private final Supplier getElement;
+ private final ILogVisualState stateLogger;
+
+ public VisualStateProvider(IImageComparator imageComparator, IElementActionRetrier actionRetrier, Supplier getElement, ILogVisualState stateLogger){
+ this.imageComparator = imageComparator;
+ this.actionRetrier = actionRetrier;
+ this.getElement = getElement;
+ this.stateLogger = stateLogger;
+ }
+
+ @Override
+ public Dimension getSize() {
+ return getLoggedValue("size", element -> {
+ final org.openqa.selenium.Dimension size = element.getSize();
+ return new Dimension(size.getWidth(), size.getHeight());
+ }, null);
+ }
+
+ @Override
+ public Point getLocation() {
+ return getLoggedValue("location", element -> {
+ final org.openqa.selenium.Point location = element.getLocation();
+ return new Point(location.getX(), location.getY());
+ }, null);
+ }
+
+ @Override
+ public BufferedImage getImage() {
+ 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..d39620c 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": "./src/test/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 55531bd..d0f8ca9 100644
--- a/src/test/java/tests/applications/browser/CachedLabel.java
+++ b/src/test/java/tests/applications/browser/CachedLabel.java
@@ -5,6 +5,9 @@
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;
import tests.applications.ICachedElement;
@@ -58,4 +61,10 @@ public ILocalizedLogger getLocalizedLogger() {
public ILocalizationManager getLocalizationManager() {
return AqualityServices.get(ILocalizationManager.class);
}
+
+ public VisualStateProvider visual() {
+ 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..5ab87be 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", "--remote-allow-origins=*"));
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/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);
}
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/tests/visualization/ImageComparatorTests.java b/src/test/java/tests/visualization/ImageComparatorTests.java
new file mode 100644
index 0000000..d0de71d
--- /dev/null
+++ b/src/test/java/tests/visualization/ImageComparatorTests.java
@@ -0,0 +1,89 @@
+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;
+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();
+ }
+
+ private Image getStartImage() {
+ return ImageFunctions.getScreenshotAsImage(DynamicLoadingForm.getStartLabel().getElement());
+ }
+
+ private Image getLoadingImage() {
+ return ImageFunctions.getScreenshotAsImage(DynamicLoadingForm.getLoadingLabel().getElement());
+ }
+
+ @Override
+ @BeforeMethod
+ public void beforeMethod() {
+ navigate(TheInternetPage.DYNAMIC_LOADING);
+ }
+
+ @Test
+ public void testGetPercentageDifferenceForSameElement() {
+ Image firstImage = getStartImage();
+ Image secondImage = getStartImage();
+
+ Assert.assertEquals(imageComparator.percentageDifference(firstImage, secondImage), 0);
+ }
+
+ @Test
+ public void testGetPercentageDifferenceForSameElementWithZeroThreshold() {
+ Image firstImage = getStartImage();
+ Image secondImage = getStartImage();
+
+ Assert.assertEquals(imageComparator.percentageDifference(firstImage, secondImage, 0), 0);
+ }
+
+ @Test
+ public void testGetPercentageDifferenceForDifferentElements() {
+ Image firstImage = getStartImage();
+ startLoading();
+ Image secondImage = getLoadingImage();
+
+ Assert.assertNotEquals(imageComparator.percentageDifference(firstImage, secondImage), 0);
+ }
+
+ @Test
+ public void testGetPercentageDifferenceForDifferentElementsWithFullThreshold() {
+ final int threshold = 1;
+ Image firstImage = getStartImage();
+ startLoading();
+ Image secondImage = getLoadingImage();
+
+ Assert.assertEquals(imageComparator.percentageDifference(firstImage, secondImage, threshold), 0);
+ }
+
+ @Test
+ public void testGetPercentageDifferenceForSimilarElements() {
+ startLoading();
+ Image firstImage = getLoadingImage();
+ DynamicLoadingForm.waitUntilLoaderChanged(firstImage.getHeight(null));
+ Image secondImage = getLoadingImage();
+
+ 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");
+ }
+}
diff --git a/src/test/java/tests/visualization/VisualStateProviderTests.java b/src/test/java/tests/visualization/VisualStateProviderTests.java
new file mode 100644
index 0000000..1ebff3e
--- /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() {
+ startLoading();
+ Image firstImage = getLoadingVisual().getImage();
+ 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);
+ Assert.assertTrue(getLoadingVisual().getDifference(firstImage, 0.6f) <= 0.1);
+ Assert.assertEquals(getLoadingVisual().getDifference(firstImage, 0.8f), 0);
+ }
+}
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());
+ }
}
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/";