io.appium.java_client.AppiumFluentWait Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of java-client Show documentation
Show all versions of java-client Show documentation
Java client for Appium Mobile Webdriver
/*
* 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 com.google.common.base.Throwables;
import io.appium.java_client.internal.ReflectionHelpers;
import lombok.AccessLevel;
import lombok.Getter;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.support.ui.FluentWait;
import org.openqa.selenium.support.ui.Sleeper;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
public class AppiumFluentWait extends FluentWait {
private Function pollingStrategy = null;
private static final Duration DEFAULT_POLL_DELAY_DURATION = Duration.ZERO;
private Duration pollDelay = DEFAULT_POLL_DELAY_DURATION;
public static class IterationInfo {
/**
* The current iteration number.
*
* @return current iteration number. It starts from 1
*/
@Getter(AccessLevel.PUBLIC) private final long number;
/**
* The amount of elapsed time.
*
* @return the amount of elapsed time
*/
@Getter(AccessLevel.PUBLIC) private final Duration elapsed;
/**
* The amount of total time.
*
* @return the amount of total time
*/
@Getter(AccessLevel.PUBLIC) private final Duration total;
/**
* The current interval.
*
* @return The actual value of current interval or the default one if it is not set
*/
@Getter(AccessLevel.PUBLIC) private final Duration interval;
/**
* The class is used to represent information about a single loop iteration in {@link #until(Function)}
* method.
*
* @param number loop iteration number, starts from 1
* @param elapsed the amount of elapsed time since the loop started
* @param total the amount of total time to run the loop
* @param interval the default time interval for each loop iteration
*/
public IterationInfo(long number, Duration elapsed, Duration total, Duration interval) {
this.number = number;
this.elapsed = elapsed;
this.total = total;
this.interval = interval;
}
}
/**
* The input value to pass to the evaluated conditions.
*
* @param input The input value to pass to the evaluated conditions.
*/
public AppiumFluentWait(T input) {
super(input);
}
/**
* Creates wait object based on {@code input} value, {@code clock} and {@code sleeper}.
*
* @param input The input value to pass to the evaluated conditions.
* @param clock The clock to use when measuring the timeout.
* @param sleeper Used to put the thread to sleep between evaluation loops.
*/
public AppiumFluentWait(T input, Clock clock, Sleeper sleeper) {
super(input, clock, sleeper);
}
/**
* Sets how long to wait before starting to evaluate condition to be true.
* The default pollDelay is {@link #DEFAULT_POLL_DELAY_DURATION}.
*
* @param pollDelay The pollDelay duration.
* @return A self reference.
*/
public AppiumFluentWait withPollDelay(Duration pollDelay) {
this.pollDelay = pollDelay;
return this;
}
private B getPrivateFieldValue(String fieldName, Class fieldType) {
return ReflectionHelpers.getPrivateFieldValue(FluentWait.class, this, fieldName, fieldType);
}
private Object getPrivateFieldValue(String fieldName) {
return getPrivateFieldValue(fieldName, Object.class);
}
protected Clock getClock() {
return getPrivateFieldValue("clock", Clock.class);
}
protected Duration getTimeout() {
return getPrivateFieldValue("timeout", Duration.class);
}
protected Duration getInterval() {
return getPrivateFieldValue("interval", Duration.class);
}
protected Sleeper getSleeper() {
return getPrivateFieldValue("sleeper", Sleeper.class);
}
@SuppressWarnings("unchecked")
protected List> getIgnoredExceptions() {
return getPrivateFieldValue("ignoredExceptions", List.class);
}
@SuppressWarnings("unchecked")
protected Supplier getMessageSupplier() {
return getPrivateFieldValue("messageSupplier", Supplier.class);
}
@SuppressWarnings("unchecked")
protected T getInput() {
return (T) getPrivateFieldValue("input");
}
/**
* Sets the strategy for polling. The default strategy is null,
* which means, that polling interval is always a constant value and is
* set by {@link #pollingEvery(Duration)} method. Otherwise the value set by that
* method might be just a helper to calculate the actual interval.
* Although, by setting an alternative polling strategy you may flexibly control
* the duration of this interval for each polling round.
* For example we'd like to wait two times longer than before each time we cannot find
* an element:
*
* final Wait<WebElement> wait = new AppiumFluentWait<>(el)
* .withPollingStrategy(info -> new Duration(info.getNumber() * 2, TimeUnit.SECONDS))
* .withTimeout(6, TimeUnit.SECONDS);
* wait.until(WebElement::isDisplayed);
*
* Or we want the next time period is Euler's number e raised to the power of current iteration
* number:
*
* final Wait<WebElement> wait = new AppiumFluentWait<>(el)
* .withPollingStrategy(info -> new Duration((long) Math.exp(info.getNumber()), TimeUnit.SECONDS))
* .withTimeout(6, TimeUnit.SECONDS);
* wait.until(WebElement::isDisplayed);
*
* Or we'd like to have some advanced algorithm, which waits longer first, but then use the default interval when it
* reaches some constant:
*
* final Wait<WebElement> wait = new AppiumFluentWait<>(el)
* .withPollingStrategy(info -> new Duration(info.getNumber() < 5
* ? 4 - info.getNumber() : info.getInterval().in(TimeUnit.SECONDS), TimeUnit.SECONDS))
* .withTimeout(30, TimeUnit.SECONDS)
* .pollingEvery(1, TimeUnit.SECONDS);
* wait.until(WebElement::isDisplayed);
*
*
* @param pollingStrategy Function instance, where the first parameter
* is the information about the current loop iteration (see {@link IterationInfo})
* and the expected result is the calculated interval. It is highly
* recommended that the value returned by this lambda is greater than zero.
* @return A self reference.
*/
public AppiumFluentWait withPollingStrategy(Function pollingStrategy) {
this.pollingStrategy = pollingStrategy;
return this;
}
/**
* Repeatedly applies this instance's input value to the given function until one of the following
* occurs:
*
* - the function returns neither null nor false,
* - the function throws an unignored exception,
* - the timeout expires,
* - the current thread is interrupted
*
.
*
* @param isTrue the parameter to pass to the expected condition
* @param The function's expected return type.
* @return The functions' return value if the function returned something different
* from null or false before the timeout expired.
* @throws TimeoutException If the timeout expires.
*/
@Override
public V until(Function super T, V> isTrue) {
final var start = getClock().instant();
// Adding pollDelay to end instant will allow to verify the condition for the expected timeout duration.
final var end = start.plus(getTimeout()).plus(pollDelay);
return performIteration(isTrue, start, end);
}
private V performIteration(Function super T, V> isTrue, Instant start, Instant end) {
var iterationNumber = 1;
Throwable lastException;
sleepInterruptibly(pollDelay);
while (true) {
try {
V value = isTrue.apply(getInput());
if (value != null && (Boolean.class != value.getClass() || Boolean.TRUE.equals(value))) {
return value;
}
// Clear the last exception; if another retry or timeout exception would
// be caused by a false or null value, the last exception is not the
// cause of the timeout.
lastException = null;
} catch (Throwable e) {
lastException = propagateIfNotIgnored(e);
}
// Check the timeout after evaluating the function to ensure conditions
// with a zero timeout can succeed.
if (end.isBefore(getClock().instant())) {
handleTimeoutException(lastException, isTrue);
}
var interval = getIntervalWithPollingStrategy(start, iterationNumber);
sleepInterruptibly(interval);
++iterationNumber;
}
}
private void handleTimeoutException(Throwable lastException, Function super T, V> isTrue) {
var message = Optional.ofNullable(getMessageSupplier())
.map(Supplier::get)
.orElseGet(() -> "waiting for " + isTrue);
var timeoutMessage = String.format(
"Expected condition failed: %s (tried for %s ms with an interval of %s ms)",
message,
getTimeout().toMillis(),
getInterval().toMillis()
);
throw timeoutException(timeoutMessage, lastException);
}
private Duration getIntervalWithPollingStrategy(Instant start, long iterationNumber) {
var interval = getInterval();
return Optional.ofNullable(pollingStrategy)
.map(strategy -> strategy.apply(new IterationInfo(
iterationNumber,
Duration.between(start, getClock().instant()), getTimeout(), interval)))
.orElse(interval);
}
private void sleepInterruptibly(Duration duration) {
try {
if (!duration.isZero() && !duration.isNegative()) {
getSleeper().sleep(duration);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new WebDriverException(e);
}
}
protected Throwable propagateIfNotIgnored(Throwable e) {
for (Class extends Throwable> ignoredException : getIgnoredExceptions()) {
if (ignoredException.isInstance(e)) {
return e;
}
}
Throwables.throwIfUnchecked(e);
throw new WebDriverException(e);
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy