Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Graphene Extension - RushEye for visual validation #170

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions extension/rusheye/README.adoc
Original file line number Diff line number Diff line change
@@ -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]
----
<dependency>
<groupId>org.jboss.arquillian.graphene</groupId>
<artifactId>arquillian-graphene-rusheye</artifactId>
</dependency>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this dependency doesn't bring you the necessary implementation - it is just the aggregator. The required dependency is arquillian-graphene-rusheye-impl

----

# Basic settings

Following +arquillian.xml+ properties should be included within *rusheye* qualifier:

[source,xml]
----
<extension qualifier="rusheye">
<property ... />
</extension>
----

|===
|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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that it is possible to use it also for test cases without page object/fragments?
https://github.com/MatousJobanek/examples/blob/master/drone-graphene/drone-grahene-simple/src/test/java/arquillian/drone/graphene/simple/GoogleUITest.java
Maybe you could start with this simple example and then jump to additional abstractions

* 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<WebElement> 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 {

//

}
----

11 changes: 11 additions & 0 deletions extension/rusheye/arquillian-graphene-rusheye-impl/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.jboss.arquillian.graphene</groupId>
<artifactId>arquillian-graphene-rusheye</artifactId>
<version>2.3.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>arquillian-graphene-rusheye-impl</artifactId>
<name>Graphene Extension: RushEye - Implementation</name>
</project>
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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 {

}
Original file line number Diff line number Diff line change
@@ -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})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about using it also for test methods...?

public @interface Snap {
String value() default "";
float onePixelThreshold() default -1f;
int similarityCutOff() default -1;
String[] masks() default {};
}
Original file line number Diff line number Diff line change
@@ -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<WebElement> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<WebElement> elements);
Ocular sleep(long time, TimeUnit timeUnit);
OcularResult compare();

}
Loading