mirror of
https://github.com/google/nomulus.git
synced 2025-05-19 18:59:35 +02:00
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
This commit is contained in:
parent
4d9db4ac8c
commit
92458f4363
24 changed files with 385 additions and 28 deletions
|
@ -14,24 +14,61 @@
|
|||
|
||||
package google.registry.webdriver;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static java.lang.Math.abs;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import java.awt.Color;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.IntStream;
|
||||
import javax.imageio.ImageIO;
|
||||
import org.openqa.selenium.OutputType;
|
||||
import org.openqa.selenium.TakesScreenshot;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.chrome.ChromeDriver;
|
||||
import org.openqa.selenium.chrome.ChromeDriverService;
|
||||
import org.openqa.selenium.chrome.ChromeOptions;
|
||||
import org.openqa.selenium.remote.RemoteWebDriver;
|
||||
|
||||
/** Implementation of {@link WebDriverPlusScreenDiffer} that uses {@link ChromeDriver}. */
|
||||
class ChromeWebDriverPlusScreenDiffer implements WebDriverPlusScreenDiffer {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
private static final String SCREENSHOT_DIR = "build/screenshots";
|
||||
private static final ChromeDriverService chromeDriverService = createChromeDriverService();
|
||||
private final WebDriver webDriver;
|
||||
|
||||
public ChromeWebDriverPlusScreenDiffer() {
|
||||
private final WebDriver webDriver;
|
||||
private final String goldensPath;
|
||||
private final int maxColorDiff;
|
||||
private final int maxPixelDiff;
|
||||
private final List<ActualScreenshot> actualScreenshots;
|
||||
|
||||
public ChromeWebDriverPlusScreenDiffer(String goldensPath, int maxColorDiff, int maxPixelDiff) {
|
||||
this.webDriver = createChromeDriver();
|
||||
this.goldensPath = goldensPath;
|
||||
this.maxColorDiff = maxColorDiff;
|
||||
this.maxPixelDiff = maxPixelDiff;
|
||||
this.actualScreenshots = Lists.newArrayList();
|
||||
}
|
||||
|
||||
private WebDriver createChromeDriver() {
|
||||
ChromeOptions chromeOptions = new ChromeOptions();
|
||||
chromeOptions.setHeadless(true);
|
||||
webDriver = new ChromeDriver(chromeDriverService, chromeOptions);
|
||||
// For Windows OS, this is required to let headless mode work properly
|
||||
chromeOptions.addArguments("disable-gpu");
|
||||
return new RemoteWebDriver(chromeDriverService.getUrl(), chromeOptions);
|
||||
}
|
||||
|
||||
private static ChromeDriverService createChromeDriverService() {
|
||||
|
@ -46,6 +83,44 @@ class ChromeWebDriverPlusScreenDiffer implements WebDriverPlusScreenDiffer {
|
|||
return chromeDriverService;
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
abstract static class ComparisonResult {
|
||||
abstract ActualScreenshot actualScreenshot();
|
||||
|
||||
abstract boolean isConsideredSimilar();
|
||||
|
||||
abstract boolean isMissingGoldenImage();
|
||||
|
||||
abstract boolean isSizeDifferent();
|
||||
|
||||
abstract int numDiffPixels();
|
||||
|
||||
static Builder builder() {
|
||||
return new AutoValue_ChromeWebDriverPlusScreenDiffer_ComparisonResult.Builder();
|
||||
}
|
||||
|
||||
@AutoValue.Builder
|
||||
abstract static class Builder {
|
||||
abstract Builder setActualScreenshot(ActualScreenshot actualScreenshot);
|
||||
|
||||
abstract Builder setIsConsideredSimilar(boolean isConsideredSimilar);
|
||||
|
||||
abstract Builder setIsMissingGoldenImage(boolean isMissingGoldenImage);
|
||||
|
||||
abstract Builder setIsSizeDifferent(boolean isSizeDifferent);
|
||||
|
||||
abstract Builder setNumDiffPixels(int numDiffPixels);
|
||||
|
||||
abstract ComparisonResult build();
|
||||
}
|
||||
}
|
||||
|
||||
static class ScreenshotNotSimilarException extends RuntimeException {
|
||||
public ScreenshotNotSimilarException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebDriver getWebDriver() {
|
||||
return webDriver;
|
||||
|
@ -53,19 +128,139 @@ class ChromeWebDriverPlusScreenDiffer implements WebDriverPlusScreenDiffer {
|
|||
|
||||
@Override
|
||||
public void diffElement(String imageKey, WebElement element) {
|
||||
// TODO(b/122650673): Add implementation for screenshots comparison. It is no-op
|
||||
// right now to prevent the test failure.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void diffPage(String imageKey) {
|
||||
// TODO(b/122650673): Add implementation for screenshots comparison. It is no-op
|
||||
// right now to prevent the test failure.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verifyAndQuit() {
|
||||
// TODO(b/122650673): Add implementation for screenshots comparison. It is no-op
|
||||
// right now to prevent the test failure.
|
||||
String errorMessage =
|
||||
actualScreenshots
|
||||
.parallelStream()
|
||||
.map(this::compareScreenshots)
|
||||
.map(
|
||||
result -> {
|
||||
String imageName = result.actualScreenshot().getImageName();
|
||||
String goldenImagePath = goldenImagePath(imageName);
|
||||
Path persistedScreenshot = persistScreenshot(result.actualScreenshot());
|
||||
|
||||
if (result.isConsideredSimilar()) {
|
||||
logger.atInfo().log(
|
||||
String.format(
|
||||
"Screenshot test for [%s] passed:\n"
|
||||
+ " - golden image location: %s\n"
|
||||
+ " - screenshot image location: %s",
|
||||
imageName, goldenImagePath, persistedScreenshot.toAbsolutePath()));
|
||||
return "";
|
||||
} else {
|
||||
String diffReason =
|
||||
String.format("it differed by %d pixels", result.numDiffPixels());
|
||||
if (result.isMissingGoldenImage()) {
|
||||
diffReason = "the golden image was missing";
|
||||
} else if (result.isSizeDifferent()) {
|
||||
diffReason = "the size of image was different";
|
||||
}
|
||||
|
||||
return String.format(
|
||||
"Screenshot test for [%s] failed because %s:\n"
|
||||
+ " - golden image location: %s\n"
|
||||
+ " - screenshot image location: %s",
|
||||
imageName,
|
||||
diffReason,
|
||||
result.isMissingGoldenImage() ? "missing" : goldenImagePath(imageName),
|
||||
persistedScreenshot.toAbsolutePath());
|
||||
}
|
||||
})
|
||||
.filter(message -> !message.isEmpty())
|
||||
.collect(joining("\n"));
|
||||
|
||||
if (!errorMessage.isEmpty()) {
|
||||
errorMessage =
|
||||
String.format(
|
||||
"Following screenshot comparison comparisonInputs failed: \n%s", errorMessage);
|
||||
logger.atSevere().log(errorMessage);
|
||||
throw new ScreenshotNotSimilarException(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private ActualScreenshot takeScreenshot(String imageKey) {
|
||||
checkArgument(webDriver instanceof TakesScreenshot);
|
||||
TakesScreenshot takesScreenshot = (TakesScreenshot) webDriver;
|
||||
return ActualScreenshot.create(imageKey, takesScreenshot.getScreenshotAs(OutputType.BYTES));
|
||||
}
|
||||
|
||||
private ComparisonResult compareScreenshots(ActualScreenshot screenshot) {
|
||||
int totalPixels = screenshot.getWidth() * screenshot.getHeight();
|
||||
Optional<BufferedImage> maybeGoldenImage = loadGoldenImageByName(screenshot.getImageName());
|
||||
ComparisonResult.Builder commonBuilder =
|
||||
ComparisonResult.builder()
|
||||
.setActualScreenshot(screenshot)
|
||||
.setIsConsideredSimilar(false)
|
||||
.setIsMissingGoldenImage(false)
|
||||
.setIsSizeDifferent(false)
|
||||
.setNumDiffPixels(totalPixels);
|
||||
|
||||
if (!maybeGoldenImage.isPresent()) {
|
||||
return commonBuilder.setIsMissingGoldenImage(true).build();
|
||||
}
|
||||
BufferedImage goldenImage = maybeGoldenImage.get();
|
||||
|
||||
if ((screenshot.getWidth() != goldenImage.getWidth())
|
||||
|| (screenshot.getHeight() != goldenImage.getHeight())) {
|
||||
return commonBuilder.setIsSizeDifferent(true).build();
|
||||
}
|
||||
|
||||
int currPixelDiff = 0;
|
||||
for (int x = 0; x < screenshot.getWidth(); x++) {
|
||||
for (int y = 0; y < screenshot.getHeight(); y++) {
|
||||
Color screenshotColor = new Color(screenshot.getRGB(x, y));
|
||||
Color goldenImageColor = new Color(goldenImage.getRGB(x, y));
|
||||
int currColorDiff =
|
||||
IntStream.of(
|
||||
abs(screenshotColor.getRed() - goldenImageColor.getRed()),
|
||||
abs(screenshotColor.getGreen() - goldenImageColor.getGreen()),
|
||||
abs(screenshotColor.getBlue() - goldenImageColor.getBlue()))
|
||||
.max()
|
||||
.getAsInt();
|
||||
if (currColorDiff > maxColorDiff) {
|
||||
currPixelDiff++;
|
||||
}
|
||||
}
|
||||
}
|
||||
commonBuilder.setNumDiffPixels(currPixelDiff);
|
||||
if (currPixelDiff <= maxPixelDiff) {
|
||||
commonBuilder.setIsConsideredSimilar(true);
|
||||
}
|
||||
|
||||
return commonBuilder.build();
|
||||
}
|
||||
|
||||
private Path persistScreenshot(ActualScreenshot screenshot) {
|
||||
File thisScreenshotDir = new File(SCREENSHOT_DIR, screenshot.getImageKey());
|
||||
thisScreenshotDir.mkdirs();
|
||||
File thisScreenshotFile =
|
||||
new File(
|
||||
thisScreenshotDir,
|
||||
Joiner.on(".").join(System.currentTimeMillis(), ActualScreenshot.IMAGE_FORMAT));
|
||||
screenshot.writeTo(thisScreenshotFile);
|
||||
return thisScreenshotFile.toPath();
|
||||
}
|
||||
|
||||
private String goldenImagePath(String imageName) {
|
||||
return Paths.get(goldensPath, imageName).toString();
|
||||
}
|
||||
|
||||
private Optional<BufferedImage> loadGoldenImageByName(String imageName) {
|
||||
File imageFile = new File(goldenImagePath(imageName));
|
||||
if (!imageFile.isFile()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
try {
|
||||
return Optional.of(ImageIO.read(imageFile));
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue