diff --git a/extension/rusheye/README.adoc b/extension/rusheye/README.adoc new file mode 100644 index 000000000..5572a46bd --- /dev/null +++ b/extension/rusheye/README.adoc @@ -0,0 +1,182 @@ += Arquillian Graphene RushEye Extension + +*rusheye* is an extension to Arquillian Graphene platform which provides the possibility to add the visual validation in your page objects/tests. In order to use it, please place this artifact configuration into Maven dependencies: + +[source,xml] +---- + + org.jboss.arquillian.graphene + arquillian-graphene-rusheye + +---- + +# Basic settings + +Following +arquillian.xml+ properties should be included within *rusheye* qualifier: + +[source,xml] +---- + + + +---- + +|=== +|Configuration Property|Description|Default Value + +|+snapshotPath+ +|location of the basleine images for compare +|/snapshot +|+resultPath+ +|location where the results with differences highlighted should be stored +|/result +|+similarityCutOff+ +|% of pixels should match for the visual validation +|100 + +|=== + +== Code example + +[source,java] +---- + +@Snap("RichFaces.png") -- (1) +public class RichFaces { + + @Drone + private WebDriver driver; + + @FindBy(css="div.left-menu") + private RichFaceLeftMenu leftmenu; + + @RushEye -- (2) + private Ocular ocular; -- (3) + + public void goTo(String demo, String skin){ + driver.get("http://showcase.richfaces.org/richfaces/component-sample.jsf?demo=" + demo + "&skin=" + skin); + } + + public RichFaceLeftMenu getLeftMenu(){ + return leftmenu; + } + + public OcularResult compare() { + return this.ocular.compare(); -- (4) + } +} +---- + +* Using ```@Snap```, Page objects / Page abstractions are mapped to the baseline(snapshot) images for the visual validation. +* The baseline images are expected to be available in the snapshotPath +* When there are no baseline images, a screenshot of the page object / page fragment is created and stored in the snapshotPath +* ```@RushEye``` injects an instance of ```Ocular``` - an utility which does the visual validation. +* ```ocular.compare``` compares the baseline against actual page object / page fragment and returns the result. + +[source,java] +---- + +@Snap("RichFaceLeftMenu.png") +public class RichFaceLeftMenu { + + @Root -- (1) + private GrapheneElement root; + + @RushEye + private Ocular ocular; + + public OcularResult compare(){ + return this.ocular.element(root) + .compare(); + } + +} + +---- + +* ```ocular.element(root).compare``` compares the baseline against actual the page fragment / given element and returns the result. + +== Excluding Elements + +Sometimes, the page object / fragment might contain an element which could contain some non-deterministic values. For example, some random number like order conformation number, date and time etc. So, We might want to exclude those elements before doing visual validation. +It can be achieved as shown here. + +[source,java] +---- + ocular.exclude(element) + .compare(); +---- + + +If we need to exclude a list of elements, + +[source,java] +---- + List elements = getElementsToBeExcluded(); + + ocular.exclude(elements) + .compare(); +---- + +or + +[source,java] +---- + ocular.exclude(element1) + .exclude(element2) + .exclude(element3) + .compare(); +---- + +== Using Dynamic Snapshots + +Ocular can use the snapshot names stored in a variable at run time as shown in this example. + +[source,java] +---- +ocular.useSnapshot(snapshot) + .element(root) + .compare() +---- + +Below ```EmployeeInfo``` page fragment is used for multiple Employee instances. So, Ocular should use the corresponding baseline specific to the current instance for isual validation. + +[source,java] +---- +@Snap("Employee-#{ID}.png") +public class ExmployeeInfo { + + @Root + private GrapheneElement root; + + @RushEye + private Ocular ocular; + + public OcularResult compare(String id){ + + return this.ocular + .replaceParameter("ID", id) + .element(root) + .compare(); + + } +} +---- + +== Similarity + +Sometimes we might not be interested in 100% match. We could use ```similarityCutOff``` to define the % of match. For the below example, If 85% percent of the pixels match, then ```Ocular``` will pass the visual validation. This will override the arquillian.xml config settings for this paeg object / fragment. + +[source,java] +---- +@Snap( + value = "Employee-#{ID}.png", + similarityCutOff = 85 + ) +public class ExmployeeInfo { + +// + +} +---- + diff --git a/extension/rusheye/arquillian-graphene-rusheye-impl/pom.xml b/extension/rusheye/arquillian-graphene-rusheye-impl/pom.xml new file mode 100644 index 000000000..bd12782ed --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-impl/pom.xml @@ -0,0 +1,11 @@ + + 4.0.0 + + org.jboss.arquillian.graphene + arquillian-graphene-rusheye + 2.3.0-SNAPSHOT + ../pom.xml + + arquillian-graphene-rusheye-impl + Graphene Extension: RushEye - Implementation + \ No newline at end of file diff --git a/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/RushEyeExtension.java b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/RushEyeExtension.java new file mode 100644 index 000000000..78d8925d5 --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/RushEyeExtension.java @@ -0,0 +1,15 @@ +package org.arquillian.graphene.rusheye; + +import org.arquillian.graphene.rusheye.configuration.RushEyeConfigurator; +import org.arquillian.graphene.rusheye.enricher.RushEyeEnricher; +import org.jboss.arquillian.core.spi.LoadableExtension; +import org.jboss.arquillian.test.spi.TestEnricher; + + +public class RushEyeExtension implements LoadableExtension{ + + public void register(ExtensionBuilder builder) { + builder.observer(RushEyeConfigurator.class); + builder.service(TestEnricher.class, RushEyeEnricher.class); + } +} diff --git a/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/annotation/RushEye.java b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/annotation/RushEye.java new file mode 100644 index 000000000..4ba73a11b --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/annotation/RushEye.java @@ -0,0 +1,12 @@ +package org.arquillian.graphene.rusheye.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD}) +public @interface RushEye { + +} diff --git a/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/annotation/Snap.java b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/annotation/Snap.java new file mode 100644 index 000000000..2b18848e9 --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/annotation/Snap.java @@ -0,0 +1,15 @@ +package org.arquillian.graphene.rusheye.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface Snap { + String value() default ""; + float onePixelThreshold() default -1f; + int similarityCutOff() default -1; + String[] masks() default {}; +} diff --git a/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/comparator/DroneImageUtil.java b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/comparator/DroneImageUtil.java new file mode 100644 index 000000000..1b6ca5a9c --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/comparator/DroneImageUtil.java @@ -0,0 +1,70 @@ +package org.arquillian.graphene.rusheye.comparator; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.util.List; + +import javax.imageio.ImageIO; + +import org.jboss.arquillian.drone.api.annotation.Default; +import org.jboss.arquillian.graphene.context.GrapheneContext; +import org.openqa.selenium.OutputType; +import org.openqa.selenium.Point; +import org.openqa.selenium.TakesScreenshot; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; + +final class DroneImageUtil { + + private static WebDriver driver = GrapheneContext.getContextFor(Default.class).getWebDriver(); + private final static AlphaComposite COMPOSITE = AlphaComposite.getInstance(AlphaComposite.CLEAR); + private final static Color TRANSPARENT = new Color(0, 0, 0, 0); + + public static BufferedImage getPageSnapshot(){ + File screen = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE); + BufferedImage page = null; + try { + page = ImageIO.read(screen); + } catch (Exception e) { + throw new RuntimeException("Unable to get page snapshot", e); + } + return page; + } + + public static BufferedImage getElementSnapshot(WebElement element){ + Point p = element.getLocation(); + int width = element.getSize().getWidth(); + int height = element.getSize().getHeight(); + return getPageSnapshot().getSubimage(p.getX(), p.getY(), width, height); + } + + public static BufferedImage maskElement(WebElement element){ + return maskArea(getPageSnapshot(), element); + } + + public static BufferedImage maskElement(BufferedImage img, WebElement element){ + return maskArea(img, element); + } + + public static BufferedImage maskElements(BufferedImage img, List elements){ + for(WebElement element: elements){ + img = maskArea(img, element); + } + return img; + } + private static BufferedImage maskArea(BufferedImage img, WebElement element){ + Graphics2D g2d = (Graphics2D) img.getGraphics(); + g2d.setComposite(COMPOSITE); + g2d.setColor(TRANSPARENT); + + Point p = element.getLocation(); + int width = element.getSize().getWidth(); + int height = element.getSize().getHeight(); + g2d.fillRect(p.getX(), p.getY(), width, height); + + return img; + } +} diff --git a/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/comparator/Ocular.java b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/comparator/Ocular.java new file mode 100644 index 000000000..28a141676 --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/comparator/Ocular.java @@ -0,0 +1,18 @@ +package org.arquillian.graphene.rusheye.comparator; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.openqa.selenium.WebElement; + +public interface Ocular { + + Ocular element(WebElement element); + Ocular replaceParameter(String param, String value); + Ocular useSnapshot(String snapshot); + Ocular exclude(WebElement element); + Ocular exclude(List elements); + Ocular sleep(long time, TimeUnit timeUnit); + OcularResult compare(); + +} diff --git a/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/comparator/OcularImpl.java b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/comparator/OcularImpl.java new file mode 100644 index 000000000..717484635 --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/comparator/OcularImpl.java @@ -0,0 +1,104 @@ +package org.arquillian.graphene.rusheye.comparator; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.arquillian.graphene.rusheye.configuration.RushEyeConfigExporter; +import org.arquillian.graphene.rusheye.configuration.RushEyeConfiguration; +import org.arquillian.graphene.rusheye.exception.RushEyeVisualComparisonException; +import org.arquillian.rusheye.comparison.ImageComparator; +import org.arquillian.rusheye.core.DefaultImageComparator; +import org.arquillian.rusheye.oneoff.ImageUtils; +import org.arquillian.rusheye.suite.ComparisonResult; +import org.openqa.selenium.WebElement; + +public class OcularImpl implements Ocular { + + private final boolean isPageFragment; + private final ImageComparator comparator; + private final Snapshot snapshot; + private final RushEyeConfiguration rusheyeConfiguration; + private WebElement element; + private final List excludedElements; + + public OcularImpl(boolean isPageFragment, SnapshotAttributes snapshotAttributes) { + this.isPageFragment = isPageFragment; + this.rusheyeConfiguration = RushEyeConfigExporter.get(); + this.comparator = new DefaultImageComparator(); + this.excludedElements = new LinkedList(); + this.snapshot = new Snapshot(snapshotAttributes); + } + + public Ocular element(WebElement element) { + this.element = element; + return this; + } + + public Ocular exclude(WebElement element) { + this.excludedElements.add(element); + return this; + } + + public Ocular exclude(List elements) { + this.excludedElements.addAll(elements); + return this; + } + + public OcularResult compare() { + ComparisonResult result = comparator.compare(this.getPattern(), + this.getSample(), + this.snapshot.getAttributes().getPerception(), + this.snapshot.getAttributes().getMasks()); + saveDiffImage(result); + return new OcularResult(result, this.snapshot.getAttributes().getSimilarityCutOff()); + } + + public Ocular useSnapshot(String snapshot) { + this.snapshot.getAttributes().setFileName(snapshot); + return this; + } + + public Ocular replaceParameter(String param, String value) { + this.snapshot.getAttributes().replaceParameter(param, value); + return this; + } + + public Ocular sleep(long time, TimeUnit timeUnit) { + try { + Thread.sleep(timeUnit.toMillis(time)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return this; + } + + private BufferedImage getPattern() { + if (this.isPageFragment) + return this.snapshot.getPattern(this.element, this.excludedElements); + else + return this.snapshot.getPattern(this.excludedElements); + } + + private BufferedImage getSample() { + BufferedImage sample; + if (this.isPageFragment) + sample = DroneImageUtil.getElementSnapshot(this.element); + else + sample = DroneImageUtil.getPageSnapshot(); + return DroneImageUtil.maskElements(sample, excludedElements); + } + + private void saveDiffImage(ComparisonResult result) { + try { + File outputfile = this.rusheyeConfiguration.getResultDefaultPath() + .resolve(this.snapshot.getAttributes().getFileName()).toFile(); + ImageUtils.writeImage(result.getDiffImage(), outputfile.getParentFile(), outputfile.getName()); + } catch (IOException e) { + throw new RushEyeVisualComparisonException("Unable to write the difference", e); + } + } +} \ No newline at end of file diff --git a/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/comparator/OcularResult.java b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/comparator/OcularResult.java new file mode 100644 index 000000000..2c4aa5c4b --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/comparator/OcularResult.java @@ -0,0 +1,53 @@ +package org.arquillian.graphene.rusheye.comparator; + +import org.arquillian.rusheye.suite.ComparisonResult; + +public class OcularResult extends ComparisonResult{ + + private final int similarityCutOff; + + public OcularResult(ComparisonResult result, int similarityCutOff) { + + super.setArea(result.getArea()); + super.setEqualsImages(result.isEqualsImages()); + super.setDiffImage(result.getDiffImage()); + super.setTotalPixels(result.getTotalPixels()); + super.setMaskedPixels(result.getMaskedPixels()); + super.setPerceptibleDiffs(result.getPerceptibleDiffs()); + super.setDifferentPixels(result.getDifferentPixels()); + super.setSmallDifferences(result.getSmallDifferences()); + super.setEqualPixels(result.getEqualPixels()); + + this.similarityCutOff = similarityCutOff; + + } + + @Override + public boolean isEqualsImages(){ + + if(super.isEqualsImages()) + return true; + else if(getPerceptibleDiffs()==0) + return true; + else if(getSimilarity() >= this.similarityCutOff) + return true; + return false; + } + + public int getSimilarity(){ + return getEqualPixels()*100 / getTotalPixels(); + } + + @Override + public String toString() { + return "OcularResult [equalsImages=" + isEqualsImages() + + ", totalPixels=" + getTotalPixels() + + ", maskedPixels=" + getMaskedPixels() + + ", perceptibleDiffs=" + getPerceptibleDiffs() + + ", differentPixels=" + getDifferentPixels() + + ", smallDifferences=" + getSmallDifferences() + + ", equalPixels=" + getEqualPixels() + + ", similarity=" + getSimilarity() + "]"; + } + +} diff --git a/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/comparator/Snapshot.java b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/comparator/Snapshot.java new file mode 100644 index 000000000..571bf35bd --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/comparator/Snapshot.java @@ -0,0 +1,61 @@ +package org.arquillian.graphene.rusheye.comparator; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import javax.imageio.ImageIO; + +import org.arquillian.graphene.rusheye.configuration.RushEyeConfigExporter; +import org.arquillian.graphene.rusheye.configuration.RushEyeConfiguration; +import org.arquillian.graphene.rusheye.exception.NoSuchPatternException; +import org.arquillian.rusheye.oneoff.ImageUtils; +import org.openqa.selenium.WebElement; + +class Snapshot { + + private final RushEyeConfiguration rusheyeConfiguration; + private final SnapshotAttributes snapshotAttributes; + private BufferedImage pattern; + + public Snapshot(SnapshotAttributes snapshotAttributes) { + this.rusheyeConfiguration = RushEyeConfigExporter.get(); + this.snapshotAttributes = snapshotAttributes; + } + + public BufferedImage getPattern(List excludedElements) { + this.setPattern(null, excludedElements); + return this.pattern; + } + + public BufferedImage getPattern(WebElement element, List excludedElements) { + this.setPattern(element, excludedElements); + return this.pattern; + } + + public SnapshotAttributes getAttributes() { + return this.snapshotAttributes; + } + + private void setPattern(WebElement element, List excludedElements) { + Path patternPath = null; + try { + patternPath = this.rusheyeConfiguration.getPatternDefaultPath().resolve(this.snapshotAttributes.getFileName()); + File patternImg = patternPath.toFile(); + if (patternImg.exists()) + this.pattern = ImageIO.read(patternImg); + else { + if (null != element) + this.pattern = DroneImageUtil.getElementSnapshot(element); + else + this.pattern = DroneImageUtil.getPageSnapshot(); + if (this.rusheyeConfiguration.getIfPatternCanBeSaved()) + ImageUtils.writeImage(this.pattern, patternImg.getParentFile(), patternImg.getName()); + } + this.pattern = DroneImageUtil.maskElements(this.pattern, excludedElements); + } catch (IOException e) { + throw new NoSuchPatternException("Unable to locate pattern @ " + patternPath, e); + } + } +} \ No newline at end of file diff --git a/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/comparator/SnapshotAttributes.java b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/comparator/SnapshotAttributes.java new file mode 100644 index 000000000..f98f12e66 --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/comparator/SnapshotAttributes.java @@ -0,0 +1,80 @@ +package org.arquillian.graphene.rusheye.comparator; + +import java.util.HashSet; +import java.util.Set; + +import org.arquillian.graphene.rusheye.configuration.RushEyeConfigExporter; +import org.arquillian.graphene.rusheye.configuration.RushEyeConfiguration; +import org.arquillian.rusheye.suite.Mask; +import org.arquillian.rusheye.suite.Perception; + +public class SnapshotAttributes { + + private final String originalParameter; + private String fileName; + private int similarityCutOff; + private Set masks; + private Perception perception; + private final RushEyeConfiguration rusheyeConfiguration; + + public SnapshotAttributes(String fileName, float onePixelThresold, int similarityCutOff, String[] masks) { + this.rusheyeConfiguration = RushEyeConfigExporter.get(); + this.fileName = fileName; + this.originalParameter = fileName; + this.setSimilarityCutOff(similarityCutOff); + this.setMasks(masks); + this.setPerception(onePixelThresold); + } + + public Perception getPerception() { + return perception; + } + + public int getSimilarityCutOff() { + return similarityCutOff; + } + + public Set getMasks() { + return masks; + } + + public void replaceParameter(String param, String value) { + this.fileName = this.originalParameter.replaceAll("\\#\\{" + param + "}", value); + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String name) { + this.fileName = name; + } + + private void setPerception(float onePixelThresold) { + this.perception = new Perception(); + if (onePixelThresold == -1f) + onePixelThresold = this.rusheyeConfiguration.getPerceptionOnePixelTreshold(); + this.perception.setOnePixelTreshold(onePixelThresold); + this.perception.setGlobalDifferenceTreshold(this.rusheyeConfiguration.getPerceptionGlobalDifferenceTreshold()); + } + + private void setMasks(String[] maskFiles) { + this.masks = new HashSet < Mask > (); + //TBD + /*try { + for (String maskFile: maskFiles) { + Path mask = this.rusheyeConfiguration.getMaskDefaultPath().resolve(maskFile); + masks.add(ImageUtils.readMaskImage(mask.toFile())); + } + } catch (Exception e) { + throw new NoSuchMaskException(maskFiles.toString(), e); + }*/ + } + + private void setSimilarityCutOff(int similarityCutOff){ + if(similarityCutOff==-1) + this.similarityCutOff = this.rusheyeConfiguration.getSimilarityCutOffPercentage(); + else + this.similarityCutOff=similarityCutOff; + } +} diff --git a/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/configuration/Configuration.java b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/configuration/Configuration.java new file mode 100644 index 000000000..eba7f285b --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/configuration/Configuration.java @@ -0,0 +1,84 @@ +package org.arquillian.graphene.rusheye.configuration; + +import org.arquillian.graphene.rusheye.exception.RushEyeConfigurationException; +import org.jboss.arquillian.core.spi.Validate; + +import java.util.HashMap; +import java.util.Map; + + +public abstract class Configuration { + + private Map configuration = new HashMap(); + + /** + * @return configuration of extension + */ + public Map getConfiguration() { + return this.configuration; + } + + + public void convertToSystemProperties(){ + for(String key : configuration.keySet()){ + System.setProperty("arquillian.extension.rusheye." + key, this.configuration.get(key)); + } + } + + /** + * Gets configuration from Arquillian descriptor and creates instance of it. + * + * @param configuration configuration of extension from arquillian.xml + * @return this + * @throws IllegalArgumentException if {@code configuration} is a null object + */ + public Configuration setConfiguration(Map configuration) { + Validate.notNull(configuration, "Properties for configuration of Arquillian Governor extension can not be a null object!"); + this.configuration = configuration; + return this; + } + + /** + * Gets value of {@code name} property. In case a value for such name does not exist or is a null object or an empty string, + * {@code defaultValue} is returned. + * + * @param name name of a property you want to get the value of + * @param defaultValue value returned in case {@code name} is a null string or it is empty + * @return value of a {@code name} property of {@code defaultValue} when {@code name} is null or empty string + * @throws IllegalArgumentException if {@code name} is a null object or an empty string or if {@code defaultValue} is a null + * object + */ + public String getProperty(String name, String defaultValue) throws IllegalStateException { + Validate.notNullOrEmpty(name, "Unable to get the configuration value of null or empty configuration key"); + Validate.notNull(defaultValue, "Unable to set configuration value of " + name + " to null object."); + + final String found = getConfiguration().get(name); + + if (found == null || found.isEmpty()) { + return defaultValue; + } else { + return found; + } + } + + /** + * Sets some property. + * + * @param name acts as a key + * @param value + * @throws IllegalArgumentException if {@code name} is null or empty or {@code value} is null + */ + public void setProperty(String name, String value) { + Validate.notNullOrEmpty(name, "Name of property can not be a null object nor an empty string!"); + Validate.notNull(value, "Value of property can not be a null object!"); + + configuration.put(name, value); + } + + /** + * Validates configuration. + * + * @throws GovernorConfigurationException when configuration of the extension is not valid + */ + public abstract void validate() throws RushEyeConfigurationException; +} \ No newline at end of file diff --git a/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/configuration/RushEyeConfigExporter.java b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/configuration/RushEyeConfigExporter.java new file mode 100644 index 000000000..f2d23c7e4 --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/configuration/RushEyeConfigExporter.java @@ -0,0 +1,13 @@ +package org.arquillian.graphene.rusheye.configuration; + +public class RushEyeConfigExporter { + private static RushEyeConfiguration rusheyeConfig; + + public static RushEyeConfiguration get(){ + return rusheyeConfig; + } + + public static void set(RushEyeConfiguration rc){ + rusheyeConfig=rc; + } +} diff --git a/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/configuration/RushEyeConfiguration.java b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/configuration/RushEyeConfiguration.java new file mode 100644 index 000000000..63790a357 --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/configuration/RushEyeConfiguration.java @@ -0,0 +1,104 @@ +package org.arquillian.graphene.rusheye.configuration; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.arquillian.graphene.rusheye.exception.RushEyeConfigurationException; + +public class RushEyeConfiguration extends Configuration{ + + //private String basePath = "src/main/resources"; + private String snapshotPath = "/snapshot"; + //private String maskPath = "/mask"; + private String resultPath = "/result"; + private final float onePixelTrehold = 0f; + private final float globalTreshold = 0f; + private final int similarityCutOff = 100; + private final boolean createSnapshot = true; //if not exists + private final boolean updateSnapshot = false; //if the result is false + + /*public Path getDefaultPath(){ + return Paths.get(getProperty("basePath", this.basePath)); + } + + public void setDefaultPath(String path){ + setProperty("basePath", path); + }*/ + + public Path getPatternDefaultPath(){ + return Paths.get(getProperty("snapshotPath", this.snapshotPath)); + } + + public void setPatternDefaultPath(String path){ + setProperty("snapshotPath", path); + } + + /*public Path getMaskDefaultPath(){ + return this.getDefaultPath().resolve(getProperty("maskPath", this.maskPath)); + } + + public void setMaskDefaultPath(String path){ + setProperty("maskPath", path); + }*/ + + public Path getResultDefaultPath(){ + return Paths.get(getProperty("resultPath", this.resultPath)); + } + + public void setResultDefaultPath(String path){ + setProperty("resultPath", path); + } + + public int getSimilarityCutOffPercentage(){ + return Integer.parseInt(getProperty("similarityCutOff", Integer.toString(this.similarityCutOff))); + } + + public void setSimilarityCutOffPercentage(int similarityCutOff){ + setProperty("similarityCutOff", Integer.toString(similarityCutOff)); + } + + public float getPerceptionOnePixelTreshold(){ + return Float.parseFloat(getProperty("perceptionOnePixelTreshold", Float.toString(this.onePixelTrehold))); + } + + public void setPerceptionOnePixelTreshold(float treshold){ + setProperty("perceptionOnePixelTreshold", Float.toString(treshold)); + } + + public float getPerceptionGlobalDifferenceTreshold(){ + return Float.parseFloat(getProperty("perceptionGlobalDifferenceTreshold", Float.toString(this.globalTreshold))); + } + + public void setPerceptionGlobalDifferenceTreshold(float treshold){ + setProperty("perceptionGlobalDifferenceTreshold", Float.toString(treshold)); + } + + public boolean getIfPatternCanBeSaved(){ + return Boolean.valueOf(getProperty("createSnapshot", Boolean.toString(this.createSnapshot))); + } + + public void setIfPatternCanBeSaved(boolean createBaseline){ + setProperty("createSnapshot", Boolean.toString(createBaseline)); + } + + public boolean getIfPatternCanBeUpdated(){ + return Boolean.valueOf(getProperty("updateSnapshot", Boolean.toString(this.updateSnapshot))); + } + + public void setIfPatternCanBeUpdated(boolean updateBaseline){ + setProperty("updateSnapshot", Boolean.toString(updateBaseline)); + } + + @Override + public void validate() throws RushEyeConfigurationException { + + if(!(this.getSimilarityCutOffPercentage()>=0 && this.getSimilarityCutOffPercentage()<=100)) + throw new IllegalArgumentException("similarityCutOff should be between 0 and 100"); + + if( !(Files.exists(this.getPatternDefaultPath())) && + Files.exists(this.getResultDefaultPath()) ) + throw new RushEyeConfigurationException("RushEye pattern/mask/result path not found"); + } + +} diff --git a/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/configuration/RushEyeConfigurator.java b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/configuration/RushEyeConfigurator.java new file mode 100644 index 000000000..d4df59df6 --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/configuration/RushEyeConfigurator.java @@ -0,0 +1,43 @@ +package org.arquillian.graphene.rusheye.configuration; + +import org.arquillian.graphene.rusheye.exception.RushEyeConfigurationException; +import org.jboss.arquillian.config.descriptor.api.ArquillianDescriptor; +import org.jboss.arquillian.config.descriptor.api.ExtensionDef; +import org.jboss.arquillian.core.api.InstanceProducer; +import org.jboss.arquillian.core.api.annotation.ApplicationScoped; +import org.jboss.arquillian.core.api.annotation.Inject; +import org.jboss.arquillian.core.api.annotation.Observes; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class RushEyeConfigurator { + + private static final Logger logger = Logger.getLogger(RushEyeConfigurator.class.getName()); + + private static final String EXTENSION_NAME = "rusheye"; + + @Inject + @ApplicationScoped + private InstanceProducer rusheyeConfiguration; + + + public void onArquillianDescriptor(@Observes ArquillianDescriptor arquillianDescriptor) throws RushEyeConfigurationException { + + final RushEyeConfiguration rusheyeConfiguration = new RushEyeConfiguration(); + + for (final ExtensionDef extension : arquillianDescriptor.getExtensions()) { + if (extension.getExtensionName().equals(EXTENSION_NAME)) { + rusheyeConfiguration.setConfiguration(extension.getExtensionProperties()); + rusheyeConfiguration.validate(); + rusheyeConfiguration.convertToSystemProperties(); + break; + } + } + + this.rusheyeConfiguration.set(rusheyeConfiguration); + RushEyeConfigExporter.set(rusheyeConfiguration); + logger.log(Level.CONFIG, "Configuration of Arquillian Graphene RushEye extension:"); + logger.log(Level.CONFIG, rusheyeConfiguration.toString()); + + } +} diff --git a/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/enricher/RushEyeEnricher.java b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/enricher/RushEyeEnricher.java new file mode 100644 index 000000000..75b29dcae --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/enricher/RushEyeEnricher.java @@ -0,0 +1,134 @@ +package org.arquillian.graphene.rusheye.enricher; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.ArrayList; +import java.util.List; + +import org.arquillian.graphene.rusheye.annotation.RushEye; +import org.arquillian.graphene.rusheye.annotation.Snap; +import org.arquillian.graphene.rusheye.comparator.OcularImpl; +import org.arquillian.graphene.rusheye.comparator.SnapshotAttributes; +import org.arquillian.graphene.rusheye.exception.RushEyeExtensionInitializationException; +import org.jboss.arquillian.graphene.fragment.Root; +import org.jboss.arquillian.test.spi.TestEnricher; + + + +public class RushEyeEnricher implements TestEnricher { + + public void enrich(Object page) { + + if(page.getClass().isAnnotationPresent(Snap.class)){ + + boolean isPageFragment = this.isRootPresent(page); + + String pattern = page.getClass().getAnnotation(Snap.class).value(); + float onePixelTreshold = page.getClass().getAnnotation(Snap.class).onePixelThreshold(); + int similarityCutOff = page.getClass().getAnnotation(Snap.class).similarityCutOff(); + String[] masks = page.getClass().getAnnotation(Snap.class).masks(); + + final SnapshotAttributes snapshotAttributes = new SnapshotAttributes(pattern, onePixelTreshold, similarityCutOff, masks); + List fields = getFieldsWithAnnotation(page.getClass(), RushEye.class); + for (Field field : fields) { + try { + setFieldValue(page, field, new OcularImpl(isPageFragment, snapshotAttributes)); + } catch (Exception e) { + throw new RushEyeExtensionInitializationException("Could not inject RushEye on field " + field, e); + } + } + } + + } + + + public Object[] resolve(Method method) { + throw new RuntimeException("RushEye can not be injected on a method"); + } + + /** + * Checks for the Root annotation + * + * @param instance - the object who might have Root annotation + * @return true if Root is found; false otherwise + */ + private boolean isRootPresent(Object page){ + List fields = getFieldsWithAnnotation(page.getClass(), Root.class); + return !fields.isEmpty(); + } + + /** + * Sets the field represented by Field object on the specified object argument to the specified new value. The new value is + * automatically unwrapped if the underlying field has a primitive type. + * + * @param instance - the object whose field should be modified + * @param field - the field that should be modified + * @param value - the new value for the field of obj being modified + */ + private void setFieldValue(final Object instance, final Field field, final Object value) { + try { + AccessController.doPrivileged(new PrivilegedExceptionAction() { + public Void run() throws IllegalArgumentException, IllegalAccessException { + field.set(instance, value); + return null; + } + }); + } catch (PrivilegedActionException e) { + final Throwable t = e.getCause(); + // Rethrow + if (t instanceof IllegalArgumentException) { + throw (IllegalArgumentException) t; + } else if (t instanceof IllegalAccessException) { + throw new IllegalStateException("Unable to set field value of " + field.getName() + " due to: " + + t.getMessage(), t.getCause()); + } else { + // No other checked Exception thrown by Class.getConstructor + try { + throw (RuntimeException) t; + } + // Just in case we've really messed up + catch (final ClassCastException cce) { + throw new RushEyeExtensionInitializationException("Obtained unchecked Exception; this code should never be reached", t); + } + } + } + } + + /** + * Returns all the fields annotated with the given annotation + * + * @param source - the class where the fields should be examined in + * @param annotationClass - the annotation the fields should be annotated with + * @return list of found fields annotated with the given annotation + */ + private List getFieldsWithAnnotation(final Class source, final Class annotationClass) { + List declaredAccessableFields = AccessController.doPrivileged(new PrivilegedAction>() { + + public List run() { + List foundFields = new ArrayList(); + Class nextSource = source; + + while (nextSource != Object.class) { + for (Field field : nextSource.getDeclaredFields()) { + + if (field.isAnnotationPresent(annotationClass)) { + if (!field.isAccessible()) { + field.setAccessible(true); + } + foundFields.add(field); + } + } + nextSource = nextSource.getSuperclass(); + } + return foundFields; + } + }); + return declaredAccessableFields; + } + +} \ No newline at end of file diff --git a/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/exception/NoSuchMaskException.java b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/exception/NoSuchMaskException.java new file mode 100644 index 000000000..63daf5aaf --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/exception/NoSuchMaskException.java @@ -0,0 +1,21 @@ +package org.arquillian.graphene.rusheye.exception; + +public class NoSuchMaskException extends RuntimeException{ + + private static final long serialVersionUID = -6912622751610135186L; + + public NoSuchMaskException() { + } + + public NoSuchMaskException(String message) { + super(message); + } + + public NoSuchMaskException(Throwable cause) { + super(cause); + } + + public NoSuchMaskException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/exception/NoSuchPatternException.java b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/exception/NoSuchPatternException.java new file mode 100644 index 000000000..d6b766e5c --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/exception/NoSuchPatternException.java @@ -0,0 +1,21 @@ +package org.arquillian.graphene.rusheye.exception; + +public class NoSuchPatternException extends RuntimeException { + + private static final long serialVersionUID = -4532972466724854705L; + + public NoSuchPatternException() { + } + + public NoSuchPatternException(String message) { + super(message); + } + + public NoSuchPatternException(Throwable cause) { + super(cause); + } + + public NoSuchPatternException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/exception/RushEyeConfigurationException.java b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/exception/RushEyeConfigurationException.java new file mode 100644 index 000000000..9ec0ec995 --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/exception/RushEyeConfigurationException.java @@ -0,0 +1,21 @@ +package org.arquillian.graphene.rusheye.exception; + +public class RushEyeConfigurationException extends RuntimeException { + + private static final long serialVersionUID = 2544120188796893890L; + + public RushEyeConfigurationException() { + } + + public RushEyeConfigurationException(String message) { + super(message); + } + + public RushEyeConfigurationException(Throwable cause) { + super(cause); + } + + public RushEyeConfigurationException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/exception/RushEyeDescriptorParsingException.java b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/exception/RushEyeDescriptorParsingException.java new file mode 100644 index 000000000..ec32b8c51 --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/exception/RushEyeDescriptorParsingException.java @@ -0,0 +1,22 @@ +package org.arquillian.graphene.rusheye.exception; + +public class RushEyeDescriptorParsingException extends RuntimeException { + + private static final long serialVersionUID = -1828106902777124637L; + + public RushEyeDescriptorParsingException() { + } + + public RushEyeDescriptorParsingException(String message) { + super(message); + } + + public RushEyeDescriptorParsingException(Throwable cause) { + super(cause); + } + + public RushEyeDescriptorParsingException(String message, Throwable cause) { + super(message, cause); + } + +} \ No newline at end of file diff --git a/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/exception/RushEyeExtensionInitializationException.java b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/exception/RushEyeExtensionInitializationException.java new file mode 100644 index 000000000..c8f470ef3 --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/exception/RushEyeExtensionInitializationException.java @@ -0,0 +1,22 @@ +package org.arquillian.graphene.rusheye.exception; + +public class RushEyeExtensionInitializationException extends RuntimeException { + + private static final long serialVersionUID = -1085706802993077668L; + + public RushEyeExtensionInitializationException() { + } + + public RushEyeExtensionInitializationException(String message) { + super(message); + } + + public RushEyeExtensionInitializationException(Throwable cause) { + super(cause); + } + + public RushEyeExtensionInitializationException(String message, Throwable cause) { + super(message, cause); + } + +} \ No newline at end of file diff --git a/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/exception/RushEyeVisualComparisonException.java b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/exception/RushEyeVisualComparisonException.java new file mode 100644 index 000000000..628f29a3a --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/java/org/arquillian/graphene/rusheye/exception/RushEyeVisualComparisonException.java @@ -0,0 +1,21 @@ +package org.arquillian.graphene.rusheye.exception; + +public class RushEyeVisualComparisonException extends RuntimeException { + + private static final long serialVersionUID = -1085706802993077668L; + + public RushEyeVisualComparisonException() { + } + + public RushEyeVisualComparisonException(String message) { + super(message); + } + + public RushEyeVisualComparisonException(Throwable cause) { + super(cause); + } + + public RushEyeVisualComparisonException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension new file mode 100644 index 000000000..327fcf99c --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-impl/src/main/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension @@ -0,0 +1 @@ +org.arquillian.graphene.rusheye.RushEyeExtension \ No newline at end of file diff --git a/extension/rusheye/arquillian-graphene-rusheye-test/arquillian.xml b/extension/rusheye/arquillian-graphene-rusheye-test/arquillian.xml new file mode 100644 index 000000000..220dcc0b2 --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-test/arquillian.xml @@ -0,0 +1,15 @@ + + + + phantomjs + 1280x1024 + + + 10 + + + src/test/resources/snapshot + src/test/resources/result + + + \ No newline at end of file diff --git a/extension/rusheye/arquillian-graphene-rusheye-test/pom.xml b/extension/rusheye/arquillian-graphene-rusheye-test/pom.xml new file mode 100644 index 000000000..eb9936b08 --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-test/pom.xml @@ -0,0 +1,28 @@ + + 4.0.0 + + org.jboss.arquillian.graphene + arquillian-graphene-rusheye + 2.3.0-SNAPSHOT + ../pom.xml + + arquillian-graphene-rusheye-test + Graphene Extension: RushEye - Test + + UTF-8 + 1.1.13.Final + 2.1.1 + ${project.version} + + + + org.jboss.arquillian.graphene + arquillian-graphene-rusheye-impl + ${project.version} + + + org.seleniumhq.selenium + selenium-server + + + \ No newline at end of file diff --git a/extension/rusheye/arquillian-graphene-rusheye-test/src/main/java/org/arquillian/graphene/rusheye/fragments/RichFaceLeftMenu.java b/extension/rusheye/arquillian-graphene-rusheye-test/src/main/java/org/arquillian/graphene/rusheye/fragments/RichFaceLeftMenu.java new file mode 100644 index 000000000..c0415f1fb --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-test/src/main/java/org/arquillian/graphene/rusheye/fragments/RichFaceLeftMenu.java @@ -0,0 +1,23 @@ +package org.arquillian.graphene.rusheye.fragments; + +import org.arquillian.graphene.rusheye.annotation.RushEye; +import org.arquillian.graphene.rusheye.annotation.Snap; +import org.arquillian.graphene.rusheye.comparator.Ocular; +import org.arquillian.graphene.rusheye.comparator.OcularResult; +import org.jboss.arquillian.graphene.GrapheneElement; +import org.jboss.arquillian.graphene.fragment.Root; + +@Snap("RichFaceLeftMenu.png") +public class RichFaceLeftMenu { + + @Root + private GrapheneElement root; + + @RushEye + private Ocular ocular; + + public OcularResult compare(){ + return this.ocular.element(root) + .compare(); + } +} diff --git a/extension/rusheye/arquillian-graphene-rusheye-test/src/main/java/org/arquillian/graphene/rusheye/pages/RichFaces.java b/extension/rusheye/arquillian-graphene-rusheye-test/src/main/java/org/arquillian/graphene/rusheye/pages/RichFaces.java new file mode 100644 index 000000000..1e824d4a2 --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-test/src/main/java/org/arquillian/graphene/rusheye/pages/RichFaces.java @@ -0,0 +1,35 @@ +package org.arquillian.graphene.rusheye.pages; + +import org.arquillian.graphene.rusheye.annotation.RushEye; +import org.arquillian.graphene.rusheye.annotation.Snap; +import org.arquillian.graphene.rusheye.comparator.Ocular; +import org.arquillian.graphene.rusheye.comparator.OcularResult; +import org.arquillian.graphene.rusheye.fragments.RichFaceLeftMenu; +import org.jboss.arquillian.drone.api.annotation.Drone; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.support.FindBy; + +@Snap("RichFaces.png") +public class RichFaces { + + @Drone + private WebDriver driver; + + @FindBy(css="div.left-menu") + private RichFaceLeftMenu leftmenu; + + @RushEye + private Ocular ocular; + + public void goTo(String demo, String skin){ + driver.get("http://showcase.richfaces.org/richfaces/component-sample.jsf?demo=" + demo + "&skin=" + skin); + } + + public RichFaceLeftMenu getLeftMenu(){ + return leftmenu; + } + + public OcularResult compare() { + return this.ocular.compare(); + } +} diff --git a/extension/rusheye/arquillian-graphene-rusheye-test/src/main/java/org/arquillian/graphene/rusheye/pages/RichFaces60PercentSimilarity.java b/extension/rusheye/arquillian-graphene-rusheye-test/src/main/java/org/arquillian/graphene/rusheye/pages/RichFaces60PercentSimilarity.java new file mode 100644 index 000000000..be56b6b92 --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-test/src/main/java/org/arquillian/graphene/rusheye/pages/RichFaces60PercentSimilarity.java @@ -0,0 +1,39 @@ +package org.arquillian.graphene.rusheye.pages; + +import org.arquillian.graphene.rusheye.annotation.RushEye; +import org.arquillian.graphene.rusheye.annotation.Snap; +import org.arquillian.graphene.rusheye.comparator.Ocular; +import org.arquillian.graphene.rusheye.comparator.OcularResult; +import org.arquillian.graphene.rusheye.fragments.RichFaceLeftMenu; +import org.jboss.arquillian.drone.api.annotation.Drone; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.support.FindBy; + +@Snap( + value="RichFaces.png", + similarityCutOff=60 + ) +public class RichFaces60PercentSimilarity { + + @Drone + private WebDriver driver; + + @FindBy(css="div.left-menu") + private RichFaceLeftMenu leftmenu; + + @RushEye + private Ocular ocular; + + public void goTo(String demo, String skin){ + driver.get("http://showcase.richfaces.org/richfaces/component-sample.jsf?demo=" + demo + "&skin=" + skin); + } + + public RichFaceLeftMenu getLeftMenu(){ + return leftmenu; + } + + public OcularResult compare() { + return this.ocular.compare(); + } + +} diff --git a/extension/rusheye/arquillian-graphene-rusheye-test/src/main/java/org/arquillian/graphene/rusheye/pages/RichFacesPoll.java b/extension/rusheye/arquillian-graphene-rusheye-test/src/main/java/org/arquillian/graphene/rusheye/pages/RichFacesPoll.java new file mode 100644 index 000000000..7eda8a2e4 --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-test/src/main/java/org/arquillian/graphene/rusheye/pages/RichFacesPoll.java @@ -0,0 +1,32 @@ +package org.arquillian.graphene.rusheye.pages; + +import org.arquillian.graphene.rusheye.annotation.RushEye; +import org.arquillian.graphene.rusheye.annotation.Snap; +import org.arquillian.graphene.rusheye.comparator.Ocular; +import org.arquillian.graphene.rusheye.comparator.OcularResult; +import org.jboss.arquillian.drone.api.annotation.Drone; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +@Snap("RichFacesPoll.png") +public class RichFacesPoll { + + @Drone + private WebDriver driver; + + @FindBy(id="form:serverDate") + private WebElement datetime; + + @RushEye + private Ocular ocular; + + public void goTo(String demo, String skin){ + driver.get("http://showcase.richfaces.org/richfaces/component-sample.jsf?demo=" + demo + "&skin=" + skin); + } + + public OcularResult compare() { + return this.ocular.exclude(datetime) + .compare(); + } +} diff --git a/extension/rusheye/arquillian-graphene-rusheye-test/src/main/java/org/arquillian/graphene/rusheye/pages/RichFacesTheme.java b/extension/rusheye/arquillian-graphene-rusheye-test/src/main/java/org/arquillian/graphene/rusheye/pages/RichFacesTheme.java new file mode 100644 index 000000000..e2f33e563 --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-test/src/main/java/org/arquillian/graphene/rusheye/pages/RichFacesTheme.java @@ -0,0 +1,31 @@ +package org.arquillian.graphene.rusheye.pages; + +import org.arquillian.graphene.rusheye.annotation.RushEye; +import org.arquillian.graphene.rusheye.annotation.Snap; +import org.arquillian.graphene.rusheye.comparator.Ocular; +import org.arquillian.graphene.rusheye.comparator.OcularResult; +import org.jboss.arquillian.drone.api.annotation.Drone; +import org.openqa.selenium.WebDriver; + +@Snap("RichFacesTheme-#{theme}.png") +public class RichFacesTheme { + + @Drone + private WebDriver driver; + + @RushEye + private Ocular ocular; + + public void goTo(String demo, String skin){ + driver.get("http://showcase.richfaces.org/richfaces/component-sample.jsf?demo=" + demo + "&skin=" + skin); + } + + public OcularResult compare(String theme) { + + return this.ocular + .replaceParameter("theme", theme) + .compare(); + } + + +} diff --git a/extension/rusheye/arquillian-graphene-rusheye-test/src/test/java/org/arquillian/graphene/rusheye/test/RichFaceBaseTest.java b/extension/rusheye/arquillian-graphene-rusheye-test/src/test/java/org/arquillian/graphene/rusheye/test/RichFaceBaseTest.java new file mode 100644 index 000000000..503baa427 --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-test/src/test/java/org/arquillian/graphene/rusheye/test/RichFaceBaseTest.java @@ -0,0 +1,34 @@ +package org.arquillian.graphene.rusheye.test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.arquillian.testng.Arquillian; +import org.testng.annotations.BeforeSuite; + +@RunAsClient +public class RichFaceBaseTest extends Arquillian{ + + private final static String SNAPSHOT_PATH = "src/test/resources/snapshot/"; + private final static String RESULT_PATH = "src/test/resources/result/"; + + @BeforeSuite + public void cleanUpBaselines() throws IOException { + this.cleanDir(SNAPSHOT_PATH); + this.cleanDir(RESULT_PATH); + } + + protected boolean isFilePresent(String path) { + return new File(SNAPSHOT_PATH + path).exists(); + } + + protected void cleanDir(String path) throws IOException{ + Files.list(Paths.get(path)) + .map(Path::toFile) + .forEach(File::delete); + } +} diff --git a/extension/rusheye/arquillian-graphene-rusheye-test/src/test/java/org/arquillian/graphene/rusheye/test/RichFacesTest.java b/extension/rusheye/arquillian-graphene-rusheye-test/src/test/java/org/arquillian/graphene/rusheye/test/RichFacesTest.java new file mode 100644 index 000000000..0dfd5ed00 --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-test/src/test/java/org/arquillian/graphene/rusheye/test/RichFacesTest.java @@ -0,0 +1,177 @@ +package org.arquillian.graphene.rusheye.test; + +import java.util.ArrayList; +import java.util.List; + +import org.arquillian.graphene.rusheye.comparator.OcularResult; +import org.arquillian.graphene.rusheye.pages.RichFaces; +import org.arquillian.graphene.rusheye.pages.RichFaces60PercentSimilarity; +import org.arquillian.graphene.rusheye.pages.RichFacesPoll; +import org.arquillian.graphene.rusheye.pages.RichFacesTheme; +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.arquillian.graphene.page.Page; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +@RunAsClient +public class RichFacesTest extends RichFaceBaseTest { + + @Page + private RichFaces richFacesPage; + + @Page + private RichFaces60PercentSimilarity richFacesPage60; + + @Page + private RichFacesPoll richFacesPoll; + + @Page + private RichFacesTheme richFacesTheme; + + + /* + * + * Page objects visual validation + * + */ + + @Test + public void visual_validation_when_there_is_no_basleine() { + + richFacesPage.goTo("repeat", "ruby"); + + Assert.assertFalse(isFilePresent("RichFaces.png")); + + // No baseline, so ocular compare will return true + // and saves a snapshot for future compare + Assert.assertTrue(richFacesPage.compare().isEqualsImages()); + } + + @Test(dependsOnMethods = { "visual_validation_when_there_is_no_basleine" }) + public void visual_validation_when_there_is_a_basleine() { + richFacesPage.goTo("repeat", "ruby"); + + Assert.assertTrue(isFilePresent("RichFaces.png")); + + // A snapshot has been saved already as part of previous test. + // so ocular compare will return the result + Assert.assertTrue(richFacesPage.compare().isEqualsImages()); + } + + @Test(dependsOnMethods = { "visual_validation_when_there_is_a_basleine" }) + public void visual_validation_when_theme_changes() { + richFacesPage.goTo("repeat", "wine"); + OcularResult result = richFacesPage.compare(); + + // A snapshot has been saved already as part of previous test. + // But there is a different theme + // so ocular compare will return the result as false + Assert.assertFalse(result.isEqualsImages()); + Assert.assertTrue(result.getSimilarity() > 60); + } + + + /* + * + * Page fragemnts visual validation + * + */ + + @Test(dependsOnMethods = { "visual_validation_when_theme_changes" }) + public void visual_validation_when_there_is_no_basleine_for_fragment() { + richFacesPage.goTo("repeat", "ruby"); + Assert.assertFalse(isFilePresent("RichFaceLeftMenu.png")); + + // Like a page object, ocular can also compare a fragment + OcularResult result = richFacesPage.getLeftMenu().compare(); + Assert.assertTrue(result.isEqualsImages()); + } + + @Test(dependsOnMethods = { "visual_validation_when_there_is_no_basleine_for_fragment" }) + public void visual_validation_when_there_is_a_basleine_for_fragment() { + richFacesPage.goTo("repeat", "wine"); + Assert.assertTrue(isFilePresent("RichFaceLeftMenu.png")); + OcularResult result = richFacesPage.getLeftMenu().compare(); + + // We load the page with different theme + // But fragment does not affect + // So the result is true + Assert.assertTrue(result.isEqualsImages()); + } + + + /* + * + * Similariy Test + * + */ + + @Test(dependsOnMethods = { "visual_validation_when_there_is_a_basleine_for_fragment" }) + public void visual_validation_for_similarity() { + richFacesPage.goTo("repeat", "classic"); + OcularResult result = richFacesPage60.compare(); + + // A snapshot has been saved already as part of previous test. + // But there is a different theme + // so ocular compare will return the result + // the result is true because we want only 60% match. Not 100% match + Assert.assertTrue(result.isEqualsImages()); + } + + + /* + * + * Non-determinitic elements exclusion + * + */ + + @Test(dependsOnMethods = { "visual_validation_for_similarity" }) + public void check_for_excluding_element() throws InterruptedException { + // There is a non-determinitic value in this page + + richFacesPage.goTo("poll", "wine"); + OcularResult beforeResult = richFacesPoll.compare(); + Assert.assertTrue(isFilePresent("RichFacesPoll.png")); + Assert.assertTrue(beforeResult.isEqualsImages()); + + // Wait for sometime to get the server date time changed + Thread.sleep(4000); + + OcularResult afterResult = richFacesPoll.compare(); + + // It should be equal because we exclude the date time element + Assert.assertTrue(afterResult.isEqualsImages()); + } + + /* + * + * Dynamic snapshot selection + * + */ + + @Test(dependsOnMethods = { "check_for_excluding_element" }) + public void check_for_runtime_selection_of_snapshot() { + + for(String theme: getAllThemes()){ + richFacesTheme.goTo("ajax", theme); + richFacesTheme.compare(theme); + Assert.assertTrue(isFilePresent("RichFacesTheme-" + theme + ".png")); + } + } + + //TestNG Data provider does not work. So going for a temp solution + private List getAllThemes() { + + List themes = new ArrayList(); + themes.add("wine"); + themes.add("ruby"); + themes.add("japanCherry"); + themes.add("emeraldTown"); + themes.add("deepMarine"); + themes.add("classic"); + themes.add("blueSky"); + return themes; + + } +} \ No newline at end of file diff --git a/extension/rusheye/arquillian-graphene-rusheye-test/src/test/resources/result/.gitignore b/extension/rusheye/arquillian-graphene-rusheye-test/src/test/resources/result/.gitignore new file mode 100644 index 000000000..32587fc85 --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-test/src/test/resources/result/.gitignore @@ -0,0 +1,11 @@ +target +test-output +.classpath +.settings +.project +chromedriver.log +nb-configuration.xml +*.iml +.idea +gh-pages +gh-pages diff --git a/extension/rusheye/arquillian-graphene-rusheye-test/src/test/resources/snapshot/.gitignore b/extension/rusheye/arquillian-graphene-rusheye-test/src/test/resources/snapshot/.gitignore new file mode 100644 index 000000000..32587fc85 --- /dev/null +++ b/extension/rusheye/arquillian-graphene-rusheye-test/src/test/resources/snapshot/.gitignore @@ -0,0 +1,11 @@ +target +test-output +.classpath +.settings +.project +chromedriver.log +nb-configuration.xml +*.iml +.idea +gh-pages +gh-pages diff --git a/extension/rusheye/pom.xml b/extension/rusheye/pom.xml new file mode 100644 index 000000000..c0fef6cf0 --- /dev/null +++ b/extension/rusheye/pom.xml @@ -0,0 +1,66 @@ + + 4.0.0 + + org.jboss.arquillian.graphene + graphene-parent + 2.3.0-SNAPSHOT + ../../pom.xml + + arquillian-graphene-rusheye + pom + Graphene Extension: RushEye - Parent + An extension for Arquillian Graphene for visual validation + + arquillian-graphene-rusheye-impl + arquillian-graphene-rusheye-test + + + UTF-8 + 1.1.13.Final + 2.1.1 + ${project.version} + + + + org.jboss.arquillian.graphene + graphene-webdriver + ${version.org.jboss.arquillian.graphene} + pom + + + org.testng + testng + 6.11 + test + + + org.jboss.arquillian.testng + arquillian-testng-standalone + + + org.arquillian.rusheye + rusheye-impl + 1.0.0 + + + + + + + org.jboss.arquillian + arquillian-bom + ${version.org.jboss.arquillian} + pom + import + + + + org.jboss.arquillian.extension + arquillian-drone-bom + ${version.org.jboss.arquillian.drone} + pom + import + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 6990214d9..ac6942702 100644 --- a/pom.xml +++ b/pom.xml @@ -25,6 +25,7 @@ impl ftest extension/screenshooter + extension/rusheye