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 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 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/";