package io.appium.java_client.utils;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.lessThan;
import static org.junit.Assert.assertThat;
import io.appium.java_client.ScreenshotState;
import org.junit.Before;
import org.junit.Test;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.time.Duration;
import java.util.Random;
public class ScreenshotStateTests {
private static final Random rand = new Random();
private static final Duration ONE_SECOND = Duration.ofSeconds(1);
private static final double MAX_SCORE = 0.999;
private ImagesGenerator randomImageOfStaticSize;
private ImagesGenerator randomImageOfRandomSize;
private ImagesGenerator staticImage;
private static class ImagesGenerator {
private boolean isRandom;
private boolean isSizeStatic;
private static final int DEFAULT_WIDTH = 100;
private static final int MIN_WIDTH = 50;
private static final int DEFAULT_HEIGHT = 100;
private static final int MIN_HEIGHT = 50;
ImagesGenerator(boolean isRandom, boolean isSizeStatic) {
this.isRandom = isRandom;
this.isSizeStatic = isSizeStatic;
}
private BufferedImage generate() {
final int width = isSizeStatic ? DEFAULT_WIDTH : MIN_WIDTH + rand.nextInt(DEFAULT_WIDTH);
final int height = isSizeStatic ? DEFAULT_HEIGHT : MIN_HEIGHT + rand.nextInt(DEFAULT_HEIGHT);
final BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
final Graphics2D g2 = result.createGraphics();
try {
g2.setColor(isRandom ? new Color(rand.nextInt(256), rand.nextInt(256),
rand.nextInt(256)) : Color.red);
g2.fill(new Rectangle2D.Float(0, 0,
isRandom ? rand.nextInt(DEFAULT_WIDTH) : DEFAULT_WIDTH / 2,
isRandom ? rand.nextInt(DEFAULT_HEIGHT) : DEFAULT_HEIGHT / 2));
} finally {
g2.dispose();
}
return result;
}
}
@Before
public void setUp() {
randomImageOfStaticSize = new ImagesGenerator(true, true);
randomImageOfRandomSize = new ImagesGenerator(true, false);
staticImage = new ImagesGenerator(false, true);
}
//region Positive Tests
@Test
public void testBasicComparisonScenario() {
final ScreenshotState currentState = new ScreenshotState(staticImage::generate)
.remember();
assertThat(currentState.verifyNotChanged(ONE_SECOND, MAX_SCORE), is(notNullValue()));
}
@Test
public void testChangedImageVerification() {
final ScreenshotState currentState = new ScreenshotState(randomImageOfStaticSize::generate)
.remember();
assertThat(currentState.verifyChanged(ONE_SECOND, MAX_SCORE), is(notNullValue()));
}
@Test
public void testChangedImageVerificationWithDifferentSize() {
final ScreenshotState currentState = new ScreenshotState(randomImageOfRandomSize::generate)
.remember();
assertThat(currentState.verifyChanged(ONE_SECOND, MAX_SCORE,
ScreenshotState.ResizeMode.REFERENCE_TO_TEMPLATE_RESOLUTION), is(notNullValue()));
}
@Test
public void testChangedImageVerificationWithCustomRememberedImage() {
final ScreenshotState currentState = new ScreenshotState(randomImageOfRandomSize::generate)
.remember(randomImageOfRandomSize.generate());
assertThat(currentState.verifyChanged(ONE_SECOND, MAX_SCORE,
ScreenshotState.ResizeMode.REFERENCE_TO_TEMPLATE_RESOLUTION), is(notNullValue()));
}
@Test
public void testChangedImageVerificationWithCustomInterval() {
final ScreenshotState currentState = new ScreenshotState(randomImageOfRandomSize::generate)
.setComparisonInterval(Duration.ofMillis(100)).remember();
assertThat(currentState.verifyChanged(ONE_SECOND, MAX_SCORE,
ScreenshotState.ResizeMode.REFERENCE_TO_TEMPLATE_RESOLUTION), is(notNullValue()));
}
@Test
public void testDirectOverlapScoreCalculation() {
final BufferedImage anImage = staticImage.generate();
final double score = ScreenshotState.getOverlapScore(anImage, anImage);
assertThat(score, is(greaterThanOrEqualTo(MAX_SCORE)));
}
@Test
public void testScreenshotComparisonUsingStaticMethod() {
BufferedImage img1 = randomImageOfStaticSize.generate();
// ImageIO.write(img1, "png", new File("img1.png"));
BufferedImage img2 = randomImageOfStaticSize.generate();
// ImageIO.write(img2, "png", new File("img2.png"));
assertThat(ScreenshotState.getOverlapScore(img1, img2), is(lessThan(MAX_SCORE)));
}
//endregion
//region Negative Tests
@Test(expected = ScreenshotState.ScreenshotComparisonError.class)
public void testDifferentSizeOfTemplates() {
new ScreenshotState(randomImageOfRandomSize::generate).remember().verifyChanged(ONE_SECOND, MAX_SCORE);
}
@Test(expected = NullPointerException.class)
public void testInvalidProvider() {
new ScreenshotState(() -> null).remember();
}
@Test(expected = ScreenshotState.ScreenshotComparisonTimeout.class)
public void testImagesComparisonExpectationFailed() {
new ScreenshotState(randomImageOfStaticSize::generate).remember().verifyNotChanged(ONE_SECOND, MAX_SCORE);
}
@Test(expected = ScreenshotState.ScreenshotComparisonTimeout.class)
public void testImagesComparisonExpectationFailed2() {
new ScreenshotState(staticImage::generate).remember().verifyChanged(ONE_SECOND, MAX_SCORE);
}
@Test(expected = ScreenshotState.ScreenshotComparisonError.class)
public void testScreenshotInitialStateHasNotBeenRemembered() {
new ScreenshotState(staticImage::generate).verifyNotChanged(ONE_SECOND, MAX_SCORE);
}
//endregion
}
package utils;
import io.appium.java_client.ComparesImages;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.Optional.ofNullable;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Base64;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.imageio.ImageIO;
public class ScreenshotState {
private static final Duration DEFAULT_INTERVAL_MS = Duration.ofMillis(500);
private BufferedImage previousScreenshot;
private final Supplier<BufferedImage> stateProvider;
private final ComparesImages comparator;
private Duration comparisonInterval = DEFAULT_INTERVAL_MS;
/**
* The class constructor accepts two arguments. The first one is image comparator, the second
* parameter is lambda function, that provides the screenshot of the necessary
* screen area to be verified for similarity.
* This lambda method is NOT called upon class creation.
* One has to invoke {@link #remember()} method in order to call it.
*
* <p>Examples of provider function with Appium driver:
* <code>
* () -> {
* final byte[] srcImage = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
* return ImageIO.read(new ByteArrayInputStream(srcImage));
* }
* </code>
* or
* <code>
* () -> {
* final byte[] srcImage = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
* final BufferedImage screenshot = ImageIO.read(new ByteArrayInputStream(srcImage));
* final WebElement element = driver.findElement(locator);
* // Can be simplified in Selenium 3.0+ by using getRect method of WebElement interface
* final Point elementLocation = element.getLocation();
* final Dimension elementSize = element.getSize();
* return screenshot.getSubimage(
* new Rectangle(elementLocation.x, elementLocation.y, elementSize.width, elementSize.height);
* }
* </code>
*
* @param comparator image comparator
* @param stateProvider lambda function, which returns a screenshot for further comparison
*/
public ScreenshotState(ComparesImages comparator, Supplier<BufferedImage> stateProvider) {
this.comparator = checkNotNull(comparator);
this.stateProvider = stateProvider;
}
public ScreenshotState(ComparesImages comparator) {
this(comparator, null);
}
/**
* Gets the interval value in ms between similarity verification rounds in <em>verify*</em> methods.
*
* @return current interval value in ms
*/
public Duration getComparisonInterval() {
return comparisonInterval;
}
/**
* Sets the interval between similarity verification rounds in <em>verify*</em> methods.
*
* @param comparisonInterval interval value. 500 ms by default
* @return self instance for chaining
*/
public ScreenshotState setComparisonInterval(Duration comparisonInterval) {
this.comparisonInterval = comparisonInterval;
return this;
}
/**
* Call this method to save the initial screenshot state.
* It is mandatory to call before any <em>verify*</em> method is invoked.
*
* @return self instance for chaining
*/
public ScreenshotState remember() {
this.previousScreenshot = stateProvider.get();
return this;
}
/**
* This method allows to pass a custom bitmap for further comparison
* instead of taking one using screenshot provider function. This might
* be useful in some advanced cases.
*
* @param customInitialState valid bitmap
* @return self instance for chaining
*/
public ScreenshotState remember(BufferedImage customInitialState) {
this.previousScreenshot = checkNotNull(customInitialState);
return this;
}
public static class ScreenshotComparisonError extends RuntimeException {
private static final long serialVersionUID = -7011854909939194466L;
ScreenshotComparisonError(Throwable reason) {
super(reason);
}
ScreenshotComparisonError(String message) {
super(message);
}
}
public static class ScreenshotComparisonTimeout extends RuntimeException {
private static final long serialVersionUID = 6336247721154252476L;
private final double currentScore;
ScreenshotComparisonTimeout(String message, double currentScore) {
super(message);
this.currentScore = currentScore;
}
public double getCurrentScore() {
return currentScore;
}
}
private ScreenshotState checkState(Function<Double, Boolean> checkerFunc, Duration timeout) {
final LocalDateTime started = LocalDateTime.now();
double score;
do {
final BufferedImage currentState = stateProvider.get();
score = getOverlapScore(ofNullable(this.previousScreenshot)
.orElseThrow(() -> new ScreenshotComparisonError("Initial screenshot state is not set. "
+ "Nothing to compare")), currentState);
if (checkerFunc.apply(score)) {
return this;
}
try {
Thread.sleep(comparisonInterval.toMillis());
} catch (InterruptedException e) {
throw new ScreenshotComparisonError(e);
}
}
while (Duration.between(started, LocalDateTime.now()).compareTo(timeout) <= 0);
throw new ScreenshotComparisonTimeout(
String.format("Screenshot comparison timed out after %s ms. Actual similarity score: %.5f",
timeout.toMillis(), score), score);
}
/**
* Verifies whether the state of the screenshot provided by stateProvider lambda function
* is changed within the given timeout.
*
* @param timeout timeout value
* @param minScore the value in range (0.0, 1.0)
* @return self instance for chaining
* @throws ScreenshotComparisonTimeout if the calculated score is still
* greater or equal to the given score after timeout happens
* @throws ScreenshotComparisonError if {@link #remember()} method has not been invoked yet
*/
public ScreenshotState verifyChanged(Duration timeout, double minScore) {
return checkState((x) -> x < minScore, timeout);
}
/**
* Verifies whether the state of the screenshot provided by stateProvider lambda function
* is not changed within the given timeout.
*
* @param timeout timeout value
* @param minScore the value in range (0.0, 1.0)
* @return self instance for chaining
* @throws ScreenshotComparisonTimeout if the calculated score is still
* less than the given score after timeout happens
* @throws ScreenshotComparisonError if {@link #remember()} method has not been invoked yet
*/
public ScreenshotState verifyNotChanged(Duration timeout, double minScore) {
return checkState((x) -> x >= minScore, timeout);
}
/**
* Compares two valid java bitmaps and calculates similarity score between them.
* Both images are expected to be of the same size/resolution. The method
* implicitly invokes {@link ComparesImages#getImagesSimilarity(byte[], byte[])}.
*
* @param refImage reference image
* @param tplImage template
* @return similarity score value in range (-1.0, 1.0]. 1.0 is returned if the images are equal
* @throws ScreenshotComparisonError if provided images are not valid or have
* different resolution
*/
public double getOverlapScore(BufferedImage refImage, BufferedImage tplImage) {
try (ByteArrayOutputStream img1 = new ByteArrayOutputStream();
ByteArrayOutputStream img2 = new ByteArrayOutputStream()) {
ImageIO.write(refImage, "png", img1);
ImageIO.write(tplImage, "png", img2);
return comparator
.getImagesSimilarity(Base64.getEncoder().encode(img1.toByteArray()),
Base64.getEncoder().encode(img2.toByteArray()))
.getScore();
} catch (IOException e) {
throw new ScreenshotComparisonError(e);
}
}
}