io.appium.java_client.ScreenshotState Maven / Gradle / Ivy
Show all versions of java-client Show documentation
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
* 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 io.appium.java_client;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import javax.imageio.ImageIO;
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 static java.util.Objects.requireNonNull;
import static java.util.Optional.ofNullable;
@Accessors(chain = true)
public class ScreenshotState {
private static final Duration DEFAULT_INTERVAL_MS = Duration.ofMillis(500);
private BufferedImage previousScreenshot;
private final Supplier stateProvider;
private final ComparesImages comparator;
/**
* Gets the interval value in ms between similarity verification rounds in verify* methods.
*
* @param comparisonInterval interval value. 500 ms by default
* @return current interval value in ms
*/
@Getter(AccessLevel.PUBLIC) @Setter(AccessLevel.PUBLIC) 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.
*
* Examples of provider function with Appium driver:
*
* () -> {
* final byte[] srcImage = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
* return ImageIO.read(new ByteArrayInputStream(srcImage));
* }
*
* or
*
* () -> {
* 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);
* }
*
*
* @param comparator image comparator
* @param stateProvider lambda function, which returns a screenshot for further comparison
*/
public ScreenshotState(ComparesImages comparator, Supplier stateProvider) {
this.comparator = requireNonNull(comparator);
this.stateProvider = stateProvider;
}
public ScreenshotState(ComparesImages comparator) {
this(comparator, null);
}
/**
* Call this method to save the initial screenshot state.
* It is mandatory to call before any verify* 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 = requireNonNull(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 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);
}
}
}