|
| 1 | +/* |
| 2 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 3 | + * you may not use this file except in compliance with the License. |
| 4 | + * See the NOTICE file distributed with this work for additional |
| 5 | + * information regarding copyright ownership. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +package io.appium.java_client; |
| 18 | + |
| 19 | +import com.google.common.base.Throwables; |
| 20 | + |
| 21 | +import org.openqa.selenium.TimeoutException; |
| 22 | +import org.openqa.selenium.WebDriverException; |
| 23 | +import org.openqa.selenium.support.ui.Clock; |
| 24 | +import org.openqa.selenium.support.ui.Duration; |
| 25 | +import org.openqa.selenium.support.ui.FluentWait; |
| 26 | +import org.openqa.selenium.support.ui.Sleeper; |
| 27 | + |
| 28 | +import java.lang.reflect.Field; |
| 29 | +import java.util.List; |
| 30 | +import java.util.concurrent.TimeUnit; |
| 31 | +import java.util.function.Function; |
| 32 | +import java.util.function.Supplier; |
| 33 | + |
| 34 | +public class AppiumFluentWait<T> extends FluentWait<T> { |
| 35 | + private Function<IterationInfo, Duration> pollingStrategy = null; |
| 36 | + |
| 37 | + public static class IterationInfo { |
| 38 | + private final long number; |
| 39 | + private final Duration elapsed; |
| 40 | + private final Duration total; |
| 41 | + private final Duration interval; |
| 42 | + |
| 43 | + /** |
| 44 | + * The class is used to represent information about a single loop iteration in {@link #until(Function)} |
| 45 | + * method. |
| 46 | + * |
| 47 | + * @param number loop iteration number, starts from 1 |
| 48 | + * @param elapsed the amount of elapsed time since the loop started |
| 49 | + * @param total the amount of total time to run the loop |
| 50 | + * @param interval the default time interval for each loop iteration |
| 51 | + */ |
| 52 | + public IterationInfo(long number, Duration elapsed, Duration total, Duration interval) { |
| 53 | + this.number = number; |
| 54 | + this.elapsed = elapsed; |
| 55 | + this.total = total; |
| 56 | + this.interval = interval; |
| 57 | + } |
| 58 | + |
| 59 | + /** |
| 60 | + * The current iteration number. |
| 61 | + * |
| 62 | + * @return current iteration number. It starts from 1 |
| 63 | + */ |
| 64 | + public long getNumber() { |
| 65 | + return number; |
| 66 | + } |
| 67 | + |
| 68 | + /** |
| 69 | + * The amount of elapsed time. |
| 70 | + * |
| 71 | + * @return the amount of elapsed time |
| 72 | + */ |
| 73 | + public Duration getElapsed() { |
| 74 | + return elapsed; |
| 75 | + } |
| 76 | + |
| 77 | + /** |
| 78 | + * The amount of total time. |
| 79 | + * |
| 80 | + * @return the amount of total time |
| 81 | + */ |
| 82 | + public Duration getTotal() { |
| 83 | + return total; |
| 84 | + } |
| 85 | + |
| 86 | + /** |
| 87 | + * The current interval. |
| 88 | + * |
| 89 | + * @return The actual value of current interval or the default one if it is not set |
| 90 | + */ |
| 91 | + public Duration getInterval() { |
| 92 | + return interval; |
| 93 | + } |
| 94 | + } |
| 95 | + |
| 96 | + /** |
| 97 | + * @param input The input value to pass to the evaluated conditions. |
| 98 | + */ |
| 99 | + public AppiumFluentWait(T input) { |
| 100 | + super(input); |
| 101 | + } |
| 102 | + |
| 103 | + /** |
| 104 | + * @param input The input value to pass to the evaluated conditions. |
| 105 | + * @param clock The clock to use when measuring the timeout. |
| 106 | + * @param sleeper Used to put the thread to sleep between evaluation loops. |
| 107 | + */ |
| 108 | + public AppiumFluentWait(T input, Clock clock, Sleeper sleeper) { |
| 109 | + super(input, clock, sleeper); |
| 110 | + } |
| 111 | + |
| 112 | + private <B> B getPrivateFieldValue(String fieldName, Class<B> fieldType) { |
| 113 | + try { |
| 114 | + final Field f = getClass().getSuperclass().getDeclaredField(fieldName); |
| 115 | + f.setAccessible(true); |
| 116 | + return fieldType.cast(f.get(this)); |
| 117 | + } catch (NoSuchFieldException | IllegalAccessException e) { |
| 118 | + throw new WebDriverException(e); |
| 119 | + } |
| 120 | + } |
| 121 | + |
| 122 | + private Object getPrivateFieldValue(String fieldName) { |
| 123 | + try { |
| 124 | + final Field f = getClass().getSuperclass().getDeclaredField(fieldName); |
| 125 | + f.setAccessible(true); |
| 126 | + return f.get(this); |
| 127 | + } catch (NoSuchFieldException | IllegalAccessException e) { |
| 128 | + throw new WebDriverException(e); |
| 129 | + } |
| 130 | + } |
| 131 | + |
| 132 | + protected Clock getClock() { |
| 133 | + return getPrivateFieldValue("clock", Clock.class); |
| 134 | + } |
| 135 | + |
| 136 | + protected Duration getTimeout() { |
| 137 | + return getPrivateFieldValue("timeout", Duration.class); |
| 138 | + } |
| 139 | + |
| 140 | + protected Duration getInterval() { |
| 141 | + return getPrivateFieldValue("interval", Duration.class); |
| 142 | + } |
| 143 | + |
| 144 | + protected Sleeper getSleeper() { |
| 145 | + return getPrivateFieldValue("sleeper", Sleeper.class); |
| 146 | + } |
| 147 | + |
| 148 | + @SuppressWarnings("unchecked") |
| 149 | + protected List<Class<? extends Throwable>> getIgnoredExceptions() { |
| 150 | + return getPrivateFieldValue("ignoredExceptions", List.class); |
| 151 | + } |
| 152 | + |
| 153 | + @SuppressWarnings("unchecked") |
| 154 | + protected Supplier<String> getMessageSupplier() { |
| 155 | + return getPrivateFieldValue("messageSupplier", Supplier.class); |
| 156 | + } |
| 157 | + |
| 158 | + @SuppressWarnings("unchecked") |
| 159 | + protected T getInput() { |
| 160 | + return (T) getPrivateFieldValue("input"); |
| 161 | + } |
| 162 | + |
| 163 | + /** |
| 164 | + * Sets the strategy for polling. The default strategy is null, |
| 165 | + * which means, that polling interval is always a constant value and is |
| 166 | + * set by {@link #pollingEvery(long, TimeUnit)} method. Otherwise the value set by that |
| 167 | + * method might be just a helper to calculate the actual interval. |
| 168 | + * Although, by setting an alternative polling strategy you may flexibly control |
| 169 | + * the duration of this interval for each polling round. |
| 170 | + * For example we'd like to wait two times longer than before each time we cannot find |
| 171 | + * an element: |
| 172 | + * <code> |
| 173 | + * final Wait<WebElement> wait = new AppiumFluentWait<>(el) |
| 174 | + * .withPollingStrategy(info -> new Duration(info.getNumber() * 2, TimeUnit.SECONDS)) |
| 175 | + * .withTimeout(6, TimeUnit.SECONDS); |
| 176 | + * wait.until(WebElement::isDisplayed); |
| 177 | + * </code> |
| 178 | + * Or we want the next time period is Euler's number e raised to the power of current iteration |
| 179 | + * number: |
| 180 | + * <code> |
| 181 | + * final Wait<WebElement> wait = new AppiumFluentWait<>(el) |
| 182 | + * .withPollingStrategy(info -> new Duration((long) Math.exp(info.getNumber()), TimeUnit.SECONDS)) |
| 183 | + * .withTimeout(6, TimeUnit.SECONDS); |
| 184 | + * wait.until(WebElement::isDisplayed); |
| 185 | + * </code> |
| 186 | + * Or we'd like to have some advanced algorithm, which waits longer first, but then use the default interval when it |
| 187 | + * reaches some constant: |
| 188 | + * <code> |
| 189 | + * final Wait<WebElement> wait = new AppiumFluentWait<>(el) |
| 190 | + * .withPollingStrategy(info -> new Duration(info.getNumber() < 5 |
| 191 | + * ? 4 - info.getNumber() : info.getInterval().in(TimeUnit.SECONDS), TimeUnit.SECONDS)) |
| 192 | + * .withTimeout(30, TimeUnit.SECONDS) |
| 193 | + * .pollingEvery(1, TimeUnit.SECONDS); |
| 194 | + * wait.until(WebElement::isDisplayed); |
| 195 | + * </code> |
| 196 | + * |
| 197 | + * @param pollingStrategy Function instance, where the first parameter |
| 198 | + * is the information about the current loop iteration (see {@link IterationInfo}) |
| 199 | + * and the expected result is the calculated interval. It is highly |
| 200 | + * recommended that the value returned by this lambda is greater than zero. |
| 201 | + * @return A self reference. |
| 202 | + */ |
| 203 | + public AppiumFluentWait<T> withPollingStrategy(Function<IterationInfo, Duration> pollingStrategy) { |
| 204 | + this.pollingStrategy = pollingStrategy; |
| 205 | + return this; |
| 206 | + } |
| 207 | + |
| 208 | + /** |
| 209 | + * Repeatedly applies this instance's input value to the given function until one of the following |
| 210 | + * occurs: |
| 211 | + * <ol> |
| 212 | + * <li>the function returns neither null nor false,</li> |
| 213 | + * <li>the function throws an unignored exception,</li> |
| 214 | + * <li>the timeout expires, |
| 215 | + * <li> |
| 216 | + * <li>the current thread is interrupted</li> |
| 217 | + * </ol> |
| 218 | + * |
| 219 | + * @param isTrue the parameter to pass to the expected condition |
| 220 | + * @param <V> The function's expected return type. |
| 221 | + * @return The functions' return value if the function returned something different |
| 222 | + * from null or false before the timeout expired. |
| 223 | + * @throws TimeoutException If the timeout expires. |
| 224 | + */ |
| 225 | + @Override |
| 226 | + public <V> V until(Function<? super T, V> isTrue) { |
| 227 | + final long start = getClock().now(); |
| 228 | + final long end = getClock().laterBy(getTimeout().in(TimeUnit.MILLISECONDS)); |
| 229 | + long iterationNumber = 1; |
| 230 | + Throwable lastException; |
| 231 | + while (true) { |
| 232 | + try { |
| 233 | + V value = isTrue.apply(getInput()); |
| 234 | + if (value != null && (Boolean.class != value.getClass() || Boolean.TRUE.equals(value))) { |
| 235 | + return value; |
| 236 | + } |
| 237 | + |
| 238 | + // Clear the last exception; if another retry or timeout exception would |
| 239 | + // be caused by a false or null value, the last exception is not the |
| 240 | + // cause of the timeout. |
| 241 | + lastException = null; |
| 242 | + } catch (Throwable e) { |
| 243 | + lastException = propagateIfNotIgnored(e); |
| 244 | + } |
| 245 | + |
| 246 | + // Check the timeout after evaluating the function to ensure conditions |
| 247 | + // with a zero timeout can succeed. |
| 248 | + if (!getClock().isNowBefore(end)) { |
| 249 | + String message = getMessageSupplier() != null ? getMessageSupplier().get() : null; |
| 250 | + |
| 251 | + String timeoutMessage = String.format( |
| 252 | + "Expected condition failed: %s (tried for %d second(s) with %s interval)", |
| 253 | + message == null ? "waiting for " + isTrue : message, |
| 254 | + getTimeout().in(TimeUnit.SECONDS), getInterval()); |
| 255 | + throw timeoutException(timeoutMessage, lastException); |
| 256 | + } |
| 257 | + |
| 258 | + try { |
| 259 | + Duration interval = getInterval(); |
| 260 | + if (pollingStrategy != null) { |
| 261 | + final IterationInfo info = new IterationInfo(iterationNumber, |
| 262 | + new Duration(getClock().now() - start, TimeUnit.MILLISECONDS), getTimeout(), |
| 263 | + interval); |
| 264 | + interval = pollingStrategy.apply(info); |
| 265 | + } |
| 266 | + getSleeper().sleep(interval); |
| 267 | + } catch (InterruptedException e) { |
| 268 | + Thread.currentThread().interrupt(); |
| 269 | + throw new WebDriverException(e); |
| 270 | + } |
| 271 | + ++iterationNumber; |
| 272 | + } |
| 273 | + } |
| 274 | + |
| 275 | + protected Throwable propagateIfNotIgnored(Throwable e) { |
| 276 | + for (Class<? extends Throwable> ignoredException : getIgnoredExceptions()) { |
| 277 | + if (ignoredException.isInstance(e)) { |
| 278 | + return e; |
| 279 | + } |
| 280 | + } |
| 281 | + Throwables.throwIfUnchecked(e); |
| 282 | + throw new WebDriverException(e); |
| 283 | + } |
| 284 | +} |
0 commit comments