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 extends Annotation> 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