Skip to content

Commit 31bbb9a

Browse files
authored
Feature/12 element state provider (#30)
* implement the default ElementStateProvider * Add tests and fixed ElementStateProvider and ConditionalWait * refactored ElementStateProviderTests * implement ElementCacheHandler and CachedElementStateProvider * corrected CachedElementStateProvider, add tests for it * refactored ElementStateProviders, moved common part to base class * refactored ElementStateProviders * extended tests for Cached web & windows elements corrected CachedElementStateProvider and ElementFinder * corrected the message in retrier tests * replace Throwable with Exception in catch block of ElementCacheHandler * add localization for logged message at CachedElementStateProvider
1 parent 4e21d59 commit 31bbb9a

30 files changed

+1317
-7
lines changed
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package aquality.selenium.core.elements;
2+
3+
import aquality.selenium.core.elements.interfaces.IElementCacheHandler;
4+
import aquality.selenium.core.localization.ILocalizedLogger;
5+
import aquality.selenium.core.waitings.IConditionalWait;
6+
import org.openqa.selenium.By;
7+
import org.openqa.selenium.NoSuchElementException;
8+
import org.openqa.selenium.StaleElementReferenceException;
9+
import org.openqa.selenium.WebElement;
10+
11+
import java.util.Arrays;
12+
import java.util.Collections;
13+
import java.util.List;
14+
import java.util.concurrent.TimeoutException;
15+
import java.util.function.BooleanSupplier;
16+
import java.util.function.Predicate;
17+
18+
/**
19+
* Provides functions to retrive the state for cached element.
20+
*/
21+
public class CachedElementStateProvider extends ElementStateProvider {
22+
23+
private final By locator;
24+
private final IConditionalWait conditionalWait;
25+
private final IElementCacheHandler elementCacheHandler;
26+
private final ILocalizedLogger localizedLogger;
27+
28+
public CachedElementStateProvider(By locator, IConditionalWait conditionalWait, IElementCacheHandler elementCacheHandler, ILocalizedLogger localizedLogger) {
29+
this.locator = locator;
30+
this.conditionalWait = conditionalWait;
31+
this.elementCacheHandler = elementCacheHandler;
32+
this.localizedLogger = localizedLogger;
33+
}
34+
35+
protected List<Class<? extends Exception>> getHandledExceptions() {
36+
return Arrays.asList(StaleElementReferenceException.class, NoSuchElementException.class);
37+
}
38+
39+
protected boolean tryInvokeFunction(Predicate<WebElement> predicate) {
40+
return tryInvokeFunction(predicate, getHandledExceptions());
41+
}
42+
43+
protected boolean tryInvokeFunction(Predicate<WebElement> predicate, List<Class<? extends Exception>> handledExceptions) {
44+
try {
45+
return predicate.test(elementCacheHandler.getElement(getZeroTImeout(), ElementState.EXISTS_IN_ANY_STATE));
46+
} catch (Exception exception) {
47+
if (handledExceptions.contains(exception.getClass())) {
48+
return false;
49+
}
50+
throw exception;
51+
}
52+
}
53+
54+
protected boolean waitForCondition(BooleanSupplier condition, String conditionName, Long timeout) {
55+
boolean result = conditionalWait.waitFor(condition, timeout, null);
56+
if (!result) {
57+
String timeoutString = timeout == null ? "" : String.format("%1$s s.", timeout);
58+
localizedLogger.warn("loc.element.not.in.state", locator, conditionName.toUpperCase(), timeoutString);
59+
}
60+
return result;
61+
}
62+
63+
@Override
64+
public boolean isClickable() {
65+
return tryInvokeFunction(elementClickable().getElementStateCondition());
66+
}
67+
68+
@Override
69+
public void waitForClickable(Long timeout) {
70+
String errorMessage = String.format("Element %1$s has not become clickable after timeout.", locator);
71+
try {
72+
conditionalWait.waitForTrue(this::isClickable, timeout, null, errorMessage);
73+
} catch (TimeoutException e) {
74+
localizedLogger.error("loc.element.not.in.state", elementClickable().getStateName(), ". ".concat(e.getMessage()));
75+
throw new org.openqa.selenium.TimeoutException(e.getMessage(), e);
76+
}
77+
}
78+
79+
@Override
80+
public boolean isDisplayed() {
81+
return !elementCacheHandler.isStale() && tryInvokeFunction(WebElement::isDisplayed);
82+
}
83+
84+
@Override
85+
public boolean waitForDisplayed(Long timeout) {
86+
return waitForCondition(() -> tryInvokeFunction(WebElement::isDisplayed), ElementState.DISPLAYED.toString(), timeout);
87+
}
88+
89+
@Override
90+
public boolean waitForNotDisplayed(Long timeout) {
91+
return waitForCondition(() -> !isDisplayed(), "invisible or absent", timeout);
92+
}
93+
94+
@Override
95+
public boolean isExist() {
96+
return !elementCacheHandler.isStale() && tryInvokeFunction(element -> true);
97+
}
98+
99+
@Override
100+
public boolean waitForExist(Long timeout) {
101+
return waitForCondition(() -> tryInvokeFunction(element -> true), ElementState.EXISTS_IN_ANY_STATE.toString(), timeout);
102+
}
103+
104+
@Override
105+
public boolean waitForNotExist(Long timeout) {
106+
return waitForCondition(() -> !isExist(), "absent", timeout);
107+
}
108+
109+
@Override
110+
public boolean isEnabled() {
111+
return tryInvokeFunction(elementEnabled().getElementStateCondition(), Collections.singletonList(StaleElementReferenceException.class));
112+
}
113+
114+
@Override
115+
public boolean waitForEnabled(Long timeout) {
116+
return waitForCondition(this::isEnabled, elementEnabled().getStateName(), timeout);
117+
}
118+
119+
@Override
120+
public boolean waitForNotEnabled(Long timeout) {
121+
return waitForCondition(() -> !isEnabled(), elementNotEnabled().getStateName(), timeout);
122+
}
123+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package aquality.selenium.core.elements;
2+
3+
import aquality.selenium.core.elements.interfaces.IElementFinder;
4+
import aquality.selenium.core.waitings.IConditionalWait;
5+
import org.openqa.selenium.By;
6+
7+
public class DefaultElementStateProvider extends ElementStateProvider {
8+
9+
private final By locator;
10+
private final IConditionalWait conditionalWait;
11+
private final IElementFinder elementFinder;
12+
13+
public DefaultElementStateProvider(By locator, IConditionalWait conditionalWait, IElementFinder elementFinder) {
14+
this.locator = locator;
15+
this.conditionalWait = conditionalWait;
16+
this.elementFinder = elementFinder;
17+
}
18+
19+
@Override
20+
public boolean isClickable() {
21+
return waitForIsClickable(getZeroTImeout(), true);
22+
}
23+
24+
@Override
25+
public void waitForClickable(Long timeout) {
26+
waitForIsClickable(timeout, false);
27+
}
28+
29+
private boolean waitForIsClickable(Long timeout, boolean catchTimeoutException) {
30+
DesiredState desiredState = elementClickable();
31+
desiredState = catchTimeoutException ? desiredState.withCatchingTimeoutException() : desiredState;
32+
return isElementInDesiredCondition(desiredState, timeout);
33+
}
34+
35+
private boolean isElementInDesiredCondition(DesiredState elementStateCondition, Long timeout) {
36+
return !elementFinder.findElements(locator, elementStateCondition, timeout).isEmpty();
37+
}
38+
39+
@Override
40+
public boolean isDisplayed() {
41+
return waitForDisplayed(getZeroTImeout());
42+
}
43+
44+
@Override
45+
public boolean waitForDisplayed(Long timeout) {
46+
return isAnyElementFound(timeout, ElementState.DISPLAYED);
47+
}
48+
49+
private boolean isAnyElementFound(Long timeout, ElementState state) {
50+
return !elementFinder.findElements(locator, state, timeout).isEmpty();
51+
}
52+
53+
@Override
54+
public boolean waitForNotDisplayed(Long timeout) {
55+
return conditionalWait.waitFor(() -> !isDisplayed(), timeout, null);
56+
}
57+
58+
@Override
59+
public boolean isExist() {
60+
return waitForExist(getZeroTImeout());
61+
}
62+
63+
@Override
64+
public boolean waitForExist(Long timeout) {
65+
return isAnyElementFound(timeout, ElementState.EXISTS_IN_ANY_STATE);
66+
}
67+
68+
@Override
69+
public boolean waitForNotExist(Long timeout) {
70+
return conditionalWait.waitFor(() -> !isExist(), timeout, null);
71+
}
72+
73+
@Override
74+
public boolean isEnabled() {
75+
return waitForEnabled(getZeroTImeout());
76+
}
77+
78+
@Override
79+
public boolean waitForEnabled(Long timeout) {
80+
return isElementInDesiredCondition(elementEnabled(), timeout);
81+
}
82+
83+
@Override
84+
public boolean waitForNotEnabled(Long timeout) {
85+
return isElementInDesiredCondition(elementNotEnabled(), timeout);
86+
}
87+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package aquality.selenium.core.elements;
2+
3+
import aquality.selenium.core.elements.interfaces.IElementCacheHandler;
4+
import aquality.selenium.core.elements.interfaces.IElementFinder;
5+
import org.openqa.selenium.By;
6+
import org.openqa.selenium.remote.RemoteWebElement;
7+
8+
/**
9+
* Implementation of {@link IElementCacheHandler}.
10+
*/
11+
public class ElementCacheHandler implements IElementCacheHandler {
12+
13+
private final By locator;
14+
private final ElementState state;
15+
private final IElementFinder finder;
16+
17+
private RemoteWebElement remoteElement;
18+
19+
public ElementCacheHandler(By locator, ElementState state, IElementFinder finder) {
20+
this.locator = locator;
21+
this.state = state;
22+
this.finder = finder;
23+
}
24+
25+
@Override
26+
public boolean isRefreshNeeded(ElementState customState) {
27+
if (!wasCached()) {
28+
return true;
29+
}
30+
try {
31+
boolean isDisplayed = remoteElement.isDisplayed();
32+
// refresh is needed only if the property is not match to expected element state
33+
ElementState requiredState = customState == null ? state : customState;
34+
return requiredState == ElementState.DISPLAYED && !isDisplayed;
35+
} catch (Exception e) {
36+
// refresh is needed if the property is not available
37+
return true;
38+
}
39+
}
40+
41+
@Override
42+
public boolean wasCached() {
43+
return remoteElement != null;
44+
}
45+
46+
@Override
47+
public RemoteWebElement getElement(Long timeout, ElementState customState) {
48+
ElementState requiredState = customState == null ? state : customState;
49+
if (isRefreshNeeded(requiredState)) {
50+
remoteElement = (RemoteWebElement) finder.findElement(locator, requiredState, timeout);
51+
}
52+
53+
return remoteElement;
54+
}
55+
}

src/main/java/aquality/selenium/core/elements/ElementFinder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ protected void handleTimeoutException(TimeoutException exception, By locator, De
7070
localizedLogger.debug("loc.elements.were.found.but.not.in.state", locator, desiredState.getStateName());
7171
}
7272
} else {
73-
String combinedMessage = String.format("%1$s: %2$s", exception.getMessage(), message);
73+
String combinedMessage = String.format("%1$s: %2$s", message, exception.getMessage());
7474
if (desiredState.isThrowingNoSuchElementException() && !wasAnyElementFound) {
7575
throw new NoSuchElementException(combinedMessage);
7676
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package aquality.selenium.core.elements;
2+
3+
import aquality.selenium.core.elements.interfaces.IElementStateProvider;
4+
import org.openqa.selenium.WebElement;
5+
6+
public abstract class ElementStateProvider implements IElementStateProvider {
7+
private static final long ZERO_TIMEOUT = 0L;
8+
9+
protected Long getZeroTImeout() {
10+
return ZERO_TIMEOUT;
11+
}
12+
13+
protected boolean isElementEnabled(WebElement element) {
14+
return element.isEnabled();
15+
}
16+
17+
protected DesiredState elementEnabled() {
18+
return new DesiredState(this::isElementEnabled, "ENABLED")
19+
.withCatchingTimeoutException()
20+
.withThrowingNoSuchElementException();
21+
}
22+
23+
protected DesiredState elementNotEnabled() {
24+
return new DesiredState(element -> !isElementEnabled(element), "NOT ENABLED")
25+
.withCatchingTimeoutException()
26+
.withThrowingNoSuchElementException();
27+
}
28+
29+
protected DesiredState elementClickable() {
30+
return new DesiredState(webElement -> webElement.isDisplayed() && webElement.isEnabled(), "CLICKABLE");
31+
}
32+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package aquality.selenium.core.elements.interfaces;
2+
3+
import aquality.selenium.core.elements.ElementState;
4+
import org.openqa.selenium.remote.RemoteWebElement;
5+
6+
/**
7+
* Allows to use cached element.
8+
*/
9+
public interface IElementCacheHandler {
10+
11+
/**
12+
* Determines is the cached element refresh needed.
13+
*
14+
* @return true if the refresh is needed (element wasn't found previously or is stale), false otherwise.
15+
*/
16+
default boolean isRefreshNeeded() {
17+
return isRefreshNeeded(null);
18+
}
19+
20+
/**
21+
* Determines is the cached element refresh needed.
22+
*
23+
* @param customState custom element's existance state used for search.
24+
* @return true if the refresh is needed (element wasn't found previously or is stale), false otherwise.
25+
*/
26+
boolean isRefreshNeeded(ElementState customState);
27+
28+
/**
29+
* Determines is the element stale.
30+
* @return true if the element was found previously and is currently stale, false otherwise.
31+
*/
32+
default boolean isStale() {
33+
return wasCached() && isRefreshNeeded();
34+
}
35+
36+
/**
37+
* Determines was the element cached previously.
38+
* @return true if the element was found and cached previously, false otherwise.
39+
*/
40+
boolean wasCached();
41+
42+
/**
43+
* Allows to get cached element.
44+
*
45+
* @param timeout timeout used to retrive the element when {@see isRefreshNeeded()} is true.
46+
* @param customState custom element's existance state used for search.
47+
* @return cached element.
48+
*/
49+
RemoteWebElement getElement(Long timeout, ElementState customState);
50+
51+
/**
52+
* Allows to get cached element.
53+
*
54+
* @param timeout timeout used to retrive the element when {@see isRefreshNeeded()} is true.
55+
* @return cached element.
56+
*/
57+
default RemoteWebElement getElement(Long timeout) {
58+
return getElement(timeout, null);
59+
}
60+
61+
/**
62+
* Allows to get cached element.
63+
*
64+
* @param customState custom element's existance state used for search.
65+
* @return cached element.
66+
*/
67+
default RemoteWebElement getElement(ElementState customState) {
68+
return getElement(null, customState);
69+
}
70+
71+
/**
72+
* Allows to get cached element.
73+
*
74+
* @return cached element.
75+
*/
76+
default RemoteWebElement getElement() {
77+
return getElement(null, null);
78+
}
79+
}

0 commit comments

Comments
 (0)