Skip to content

Commit 83c17c1

Browse files
Merge pull request #613 from TikhomirovSergey/mykola-mokhnach-appium_fluent_wait
The addition to the #612
2 parents 597e824 + 9510373 commit 83c17c1

File tree

4 files changed

+388
-6
lines changed

4 files changed

+388
-6
lines changed
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
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&lt;WebElement&gt; wait = new AppiumFluentWait&lt;&gt;(el)
174+
* .withPollingStrategy(info -&gt; 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&lt;WebElement&gt; wait = new AppiumFluentWait&lt;&gt;(el)
182+
* .withPollingStrategy(info -&gt; 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&lt;WebElement&gt; wait = new AppiumFluentWait&lt;&gt;(el)
190+
* .withPollingStrategy(info -&gt; new Duration(info.getNumber() &lt; 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+
}

src/main/java/io/appium/java_client/remote/AppiumProtocolHandShake.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import com.google.gson.JsonElement;
3131
import com.google.gson.JsonObject;
3232
import com.google.gson.JsonParser;
33+
3334
import org.openqa.selenium.Capabilities;
3435
import org.openqa.selenium.SessionNotCreatedException;
3536
import org.openqa.selenium.WebDriverException;
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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.appium;
18+
19+
import static org.hamcrest.MatcherAssert.assertThat;
20+
import static org.hamcrest.core.Is.is;
21+
import static org.hamcrest.core.IsEqual.equalTo;
22+
23+
import io.appium.java_client.AppiumFluentWait;
24+
25+
import org.junit.Assert;
26+
import org.junit.Test;
27+
import org.openqa.selenium.TimeoutException;
28+
import org.openqa.selenium.support.ui.Duration;
29+
import org.openqa.selenium.support.ui.SystemClock;
30+
import org.openqa.selenium.support.ui.Wait;
31+
32+
import java.util.concurrent.TimeUnit;
33+
import java.util.concurrent.atomic.AtomicInteger;
34+
import java.util.function.Function;
35+
36+
public class AppiumFluentWaitTest {
37+
private static class FakeElement {
38+
public boolean isDisplayed() {
39+
return false;
40+
}
41+
}
42+
43+
@Test(expected = TimeoutException.class)
44+
public void testDefaultStrategy() {
45+
final FakeElement el = new FakeElement();
46+
final Wait<FakeElement> wait = new AppiumFluentWait<>(el, new SystemClock(), duration -> {
47+
assertThat(duration.in(TimeUnit.SECONDS), is(equalTo(1L)));
48+
Thread.sleep(duration.in(TimeUnit.MILLISECONDS));
49+
}).withPollingStrategy(AppiumFluentWait.IterationInfo::getInterval)
50+
.withTimeout(3, TimeUnit.SECONDS)
51+
.pollingEvery(1, TimeUnit.SECONDS);
52+
wait.until(FakeElement::isDisplayed);
53+
Assert.fail("TimeoutException is expected");
54+
}
55+
56+
@Test
57+
public void testCustomStrategyOverridesDefaultInterval() {
58+
final FakeElement el = new FakeElement();
59+
final AtomicInteger callsCounter = new AtomicInteger(0);
60+
final Wait<FakeElement> wait = new AppiumFluentWait<>(el, new SystemClock(), duration -> {
61+
callsCounter.incrementAndGet();
62+
assertThat(duration.in(TimeUnit.SECONDS), is(equalTo(2L)));
63+
Thread.sleep(duration.in(TimeUnit.MILLISECONDS));
64+
}).withPollingStrategy(info -> new Duration(2, TimeUnit.SECONDS))
65+
.withTimeout(3, TimeUnit.SECONDS)
66+
.pollingEvery(1, TimeUnit.SECONDS);
67+
try {
68+
wait.until(FakeElement::isDisplayed);
69+
Assert.fail("TimeoutException is expected");
70+
} catch (TimeoutException e) {
71+
// this is expected
72+
assertThat(callsCounter.get(), is(equalTo(2)));
73+
}
74+
}
75+
76+
@Test
77+
public void testIntervalCalculationForCustomStrategy() {
78+
final FakeElement el = new FakeElement();
79+
final AtomicInteger callsCounter = new AtomicInteger(0);
80+
// Linear dependency
81+
final Function<Long, Long> pollingStrategy = x -> x * 2;
82+
final Wait<FakeElement> wait = new AppiumFluentWait<>(el, new SystemClock(), duration -> {
83+
int callNumber = callsCounter.incrementAndGet();
84+
assertThat(duration.in(TimeUnit.SECONDS), is(equalTo(pollingStrategy.apply((long) callNumber))));
85+
Thread.sleep(duration.in(TimeUnit.MILLISECONDS));
86+
}).withPollingStrategy(info -> new Duration(pollingStrategy.apply(info.getNumber()), TimeUnit.SECONDS))
87+
.withTimeout(4, TimeUnit.SECONDS)
88+
.pollingEvery(1, TimeUnit.SECONDS);
89+
try {
90+
wait.until(FakeElement::isDisplayed);
91+
Assert.fail("TimeoutException is expected");
92+
} catch (TimeoutException e) {
93+
// this is expected
94+
assertThat(callsCounter.get(), is(equalTo(2)));
95+
}
96+
}
97+
}

0 commit comments

Comments
 (0)