google-nomulus/javatests/google/registry/webdriver/WebDriverRule.java
shicong 92458f4363 Implement screenshot comparison
This change added the implementation of screenshot comparison with
ChromeWebDriver which will be used in the open-source code base.

This change also set a default window size(1200x2000) for each screenshot
test. This is to make the size of screenshot deterministic to help
build the screenshot comparison tests.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=235539713
2019-03-05 14:13:13 -05:00

297 lines
10 KiB
Java

// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.webdriver;
import static com.google.common.io.Resources.getResource;
import static java.util.stream.Collectors.joining;
import static org.apache.commons.text.StringEscapeUtils.escapeEcmaScript;
import com.google.common.base.Preconditions;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import org.junit.rules.ExternalResource;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.openqa.selenium.By;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.Dimension;
import org.openqa.selenium.HasCapabilities;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.HasInputDevices;
import org.openqa.selenium.interactions.Keyboard;
import org.openqa.selenium.interactions.Mouse;
import org.openqa.selenium.remote.DesiredCapabilities;
/** WebDriver delegate JUnit Rule that sends Sponge a screenshot on failure. */
public final class WebDriverRule extends ExternalResource
implements WebDriver, HasInputDevices, TakesScreenshot, JavascriptExecutor, HasCapabilities {
private static final int WAIT_FOR_ELEMENTS_POLLING_INTERVAL_MS = 10;
private static final int WAIT_FOR_ELEMENTS_BONUS_DELAY_MS = 150;
// The maximum difference between pixels that would be considered as "identical". Calculated as
// the sum of the absolute difference between the values of the RGB channels. So a 120,30,200
// reference pixel and a 122,31,193 result pixel have a difference of 10, and would be considered
// identical if MAX_COLOR_DIFF=10
private static final int MAX_COLOR_DIFF = 10;
// Percent of pixels that are allowed to be different (more than the MAX_COLOR_DIFF) between the
// images while still considering the test to have passed. Useful if there are a very small
// number of pixels that vary (usually on the corners of "rounded" buttons or similar)
private static final int MAX_PIXEL_DIFF = 0;
// Default size of the browser window when taking screenshot. Having a fixed size of window can
// help make visual regression test deterministic.
private static final Dimension DEFAULT_WINDOW_SIZE = new Dimension(1200, 2000);
private static final String GOLDENS_PATH =
getResource(WebDriverRule.class, "scuba_goldens/chrome-linux").getFile();
private WebDriver driver;
private WebDriverPlusScreenDiffer webDriverPlusScreenDiffer;
// Prefix to use for golden image files, will be set to ClassName_MethodName once the test
// starts. Will be added a user-given imageKey as a suffix, and of course a '.png' at the end.
private String imageNamePrefix = null;
@Override
public Statement apply(Statement base, Description description) {
if (imageNamePrefix == null) {
String className = description.getTestClass().getSimpleName();
String methodName = description.getMethodName();
String unsanitizedName = className + "_" + methodName;
// remove all of the special wildcard characters so they don't exist in filenames.
imageNamePrefix = unsanitizedName.replaceAll("[*?~\"\\[\\]]", "");
}
return super.apply(base, description);
}
@Override
protected void before() {
webDriverPlusScreenDiffer =
new ChromeWebDriverPlusScreenDiffer(GOLDENS_PATH, MAX_COLOR_DIFF, MAX_PIXEL_DIFF);
driver = webDriverPlusScreenDiffer.getWebDriver();
// non-zero timeout so findByElement will wait for the element to appear
driver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS);
driver.manage().window().setSize(DEFAULT_WINDOW_SIZE);
}
@Override
protected void after() {
webDriverPlusScreenDiffer.verifyAndQuit();
try {
driver.quit();
} finally {
driver = null;
}
}
/** @see #get(String) */
public void get(URL url) {
driver.get(url.toString());
}
/** Waits indefinitely for an element to appear on the page, then returns it. */
public WebElement waitForElement(By by) throws InterruptedException {
while (true) {
List<WebElement> elements = findElements(by);
if (!elements.isEmpty()) {
Thread.sleep(WAIT_FOR_ELEMENTS_BONUS_DELAY_MS);
return elements.get(0);
}
Thread.sleep(WAIT_FOR_ELEMENTS_POLLING_INTERVAL_MS);
}
}
/** Waits for element matching {@code by} whose {@code attribute} satisfies {@code predicate}. */
public WebElement waitForAttribute(
By by, String attribute, Predicate</*@Nullable*/ ? super CharSequence> predicate)
throws InterruptedException {
while (true) {
for (WebElement element : findElements(by)) {
if (predicate.test(element.getAttribute(attribute))) {
Thread.sleep(WAIT_FOR_ELEMENTS_BONUS_DELAY_MS);
return element;
}
}
Thread.sleep(WAIT_FOR_ELEMENTS_POLLING_INTERVAL_MS);
}
}
/** Sets value of input fields, where {@code fields} key is the {@code id=""} attribute. */
public void setFormFieldsById(Map<String, String> fields) {
executeScript(
fields.entrySet().stream()
.map(
entry ->
String.format(
"document.getElementById('%s').value = '%s';",
escapeEcmaScript(entry.getKey()), escapeEcmaScript(entry.getValue())))
.collect(joining("\n")));
}
/**
* Checks that the screenshot of the element matches the golden image by pixel comparison.
*
* <p>On mismatch, the test will continue until the end of the test (and then fail). This is so
* other screenshot matches can be executed in the same function - allowing you to approve /
* reject all at once.
*
* @param imageKey a unique name such that by prepending the calling class name and method name in
* the format of ClassName_MethodName_<imageKey> will uniquely identify golden image.
* @param element the element on the page to be compared
*/
public void diffElement(String imageKey, WebElement element) {
webDriverPlusScreenDiffer.diffElement(getUniqueName(imageKey), element);
}
/**
* Checks that the screenshot of the element matches the golden image by pixel comparison.
*
* <p>On mismatch, the test will continue until the end of the test (and then fail). This is so
* other screenshot matches can be executed in the same function - allowing you to approve /
* reject all at once.
*
* @param imageKey a unique name such that by prepending the calling class name and method name in
* the format of ClassName_MethodName_<imageKey> will uniquely identify golden image.
* @param by {@link By} which locates the element on the page to be compared
*/
public void diffElement(String imageKey, By by) {
diffElement(imageKey, driver.findElement(by));
}
/**
* Checks that the screenshot matches the golden image by pixel comparison.
*
* <p>On mismatch, the test will continue until the end of the test (and then fail). This is so
* other screenshot matches can be executed in the same function - allowing you to approve /
* reject all at once.
*
* @param imageKey a unique name such that by prepending the calling class name and method name in
* the format of ClassName_MethodName_<imageKey> will uniquely identify golden image.
*/
public void diffPage(String imageKey) {
webDriverPlusScreenDiffer.diffPage(getUniqueName(imageKey));
}
@Override
public void get(String url) {
driver.get(url);
}
@Override
public String getCurrentUrl() {
return driver.getCurrentUrl();
}
@Override
public String getTitle() {
return driver.getTitle();
}
@Override
public List<WebElement> findElements(By by) {
return driver.findElements(by);
}
@Override
public WebElement findElement(By by) {
return driver.findElement(by);
}
@Override
public String getPageSource() {
return driver.getPageSource();
}
@Override
public void close() {
driver.close();
}
@Override
public void quit() {
driver.quit();
}
@Override
public Set<String> getWindowHandles() {
return driver.getWindowHandles();
}
@Override
public String getWindowHandle() {
return driver.getWindowHandle();
}
@Override
public TargetLocator switchTo() {
return driver.switchTo();
}
@Override
public Navigation navigate() {
return driver.navigate();
}
@Override
public Options manage() {
return driver.manage();
}
@Override
public Keyboard getKeyboard() {
return ((HasInputDevices) driver).getKeyboard();
}
@Override
public Mouse getMouse() {
return ((HasInputDevices) driver).getMouse();
}
@Override
public <X> X getScreenshotAs(OutputType<X> target) throws WebDriverException {
return ((TakesScreenshot) driver).getScreenshotAs(target);
}
@Override
public Object executeScript(String script, Object... args) {
return ((JavascriptExecutor) driver).executeScript(script, args);
}
@Override
public Object executeAsyncScript(String script, Object... args) {
return ((JavascriptExecutor) driver).executeAsyncScript(script, args);
}
@Override
public Capabilities getCapabilities() {
return new DesiredCapabilities();
}
private String getUniqueName(String imageKey) {
Preconditions.checkNotNull(imageNamePrefix);
return imageNamePrefix + "_" + imageKey;
}
}