Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

import io.appium.java_client.pagefactory.bys.ContentMappedBy;
import io.appium.java_client.pagefactory.locator.CacheableLocator;
import io.appium.java_client.support.pagefactory.ByAll;
import io.appium.java_client.support.pagefactory.ByChained;
import io.appium.java_client.support.ui.FluentWait;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
Expand Down Expand Up @@ -197,6 +199,74 @@ public List<WebElement> findElements() {
return result;
}

/**
* First element of each alternative locator after the one that produced {@code resolvedRoot}.
* A widget annotated with {@link LocatorGroupStrategy#ALL_POSSIBLE} is that set of locators.
* Chained locators stay one root: a miss there is not a signal to try another chain.
*
* @param resolvedRoot the root already searched for a nested element
* @return later roots, or an empty list when this locator has no further alternatives
*/
List<WebElement> findFurtherRoots(WebElement resolvedRoot) {
SearchContext context = getSearchContext().orElse(null);
if (resolvedRoot == null || context == null) {
return List.of();
}

By[] alternatives = alternativeLocators(mappedLocator(context));
if (alternatives == null) {
return List.of();
}

List<WebElement> furtherRoots = new ArrayList<>();
boolean skippedResolvedLocator = false;
for (By candidate : alternatives) {
List<WebElement> matches = context.findElements(candidate);
if (matches.isEmpty()) {
continue;
}
if (!skippedResolvedLocator) {
skippedResolvedLocator = true;
continue;
}
furtherRoots.add(matches.get(0));
}
return furtherRoots;
}

private By mappedLocator(SearchContext context) {
By effective = getBy(by, context);
if (effective instanceof ContentMappedBy) {
return ((ContentMappedBy) effective).getCurrentLocator();
}
return effective;
}

/**
* Locators that are alternatives for one another, or null when {@code by} is a single
* locator or a chain. A chain of one locator is unwrapped so {@code @AndroidFindAll}
* is treated the same way as repeated {@code @AndroidFindBy} annotations.
*/
private static By[] alternativeLocators(By by) {
if (by instanceof ByAll) {
By[] nested = ((ByAll) by).getBys();
if (nested.length == 1) {
By[] unwrapped = alternativeLocators(nested[0]);
if (unwrapped != null) {
return unwrapped;
}
}
return nested.length > 1 ? nested : null;
}
if (by instanceof ByChained) {
By[] nested = ((ByChained) by).getBys();
if (nested.length == 1) {
return alternativeLocators(nested[0]);
}
}
return null;
}

@Override public boolean isLookUpCached() {
return shouldCache;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import io.appium.java_client.pagefactory.locator.CacheableLocator;
import io.appium.java_client.support.PageFactory;
import org.jspecify.annotations.Nullable;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;

Expand All @@ -30,6 +31,7 @@
import java.lang.reflect.Modifier;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;

Expand Down Expand Up @@ -90,34 +92,71 @@ public WidgetInterceptor(
@Override
protected Object getObject(WebElement element, Method method, Object[] args) throws Throwable {
ContentType type = getCurrentContentType(element);
WebElement cachedElement = cachedElementReference == null ? null : cachedElementReference.get();
if (cachedElement == null || !cachedInstances.containsKey(type)
|| locator != null && !((CacheableLocator) locator).isLookUpCached()
) {
cachedElementReference = new WeakReference<>(element);

Constructor<? extends Widget> constructor = instantiationMap.get(type);
Class<? extends Widget> clazz = constructor.getDeclaringClass();

if (Modifier.isAbstract(clazz.getModifiers())) {
throw new InstantiationException(
String.format("%s is abstract so it cannot be instantiated", clazz.getName())
);
if (isCached(type)) {
return invoke(cachedInstances.get(type), method, args);
}

rejectAbstractWidget(type);
try {
return bindAndInvoke(type, element, method, args);
} catch (NoSuchElementException firstMiss) {
// The first ALL_POSSIBLE root matched, but the nested lookup did not.
// Try each later root with the same nested locators.
NoSuchElementException lastMiss = firstMiss;
for (WebElement root : furtherRoots(element)) {
try {
return bindAndInvoke(type, root, method, args);
} catch (NoSuchElementException laterMiss) {
lastMiss = laterMiss;
}
}
throw lastMiss;
}
}

private boolean isCached(ContentType type) {
WebElement cachedElement = cachedElementReference == null ? null : cachedElementReference.get();
if (cachedElement == null || !cachedInstances.containsKey(type)) {
return false;
}
return locator == null || ((CacheableLocator) locator).isLookUpCached();
}

Widget widget = constructor.newInstance(element);
cachedInstances.put(type, widget);
PageFactory.initElements(
new AppiumFieldDecorator(new WeakReference<>(widget), duration, defaultLocatorGroupStrategy),
widget
private void rejectAbstractWidget(ContentType type) throws InstantiationException {
Class<? extends Widget> clazz = instantiationMap.get(type).getDeclaringClass();
if (Modifier.isAbstract(clazz.getModifiers())) {
throw new InstantiationException(
String.format("%s is abstract so it cannot be instantiated", clazz.getName())
);
}
}

private Object bindAndInvoke(ContentType type, WebElement root, Method method, Object[] args) throws Throwable {
Widget widget = instantiationMap.get(type).newInstance(root);
PageFactory.initElements(
new AppiumFieldDecorator(new WeakReference<>(widget), duration, defaultLocatorGroupStrategy),
widget
);
Object result = invoke(widget, method, args);
cachedElementReference = new WeakReference<>(root);
cachedInstances.put(type, widget);
return result;
}

private static Object invoke(Widget widget, Method method, Object[] args) throws Throwable {
try {
method.setAccessible(true);
return method.invoke(cachedInstances.get(type), args);
} catch (Throwable t) {
throw extractReadableException(t);
return method.invoke(widget, args);
} catch (Throwable thrown) {
throw extractReadableException(thrown);
}
}

private List<WebElement> furtherRoots(WebElement resolvedRoot) {
if (!(locator instanceof AppiumElementLocator)) {
return List.of();
}
return ((AppiumElementLocator) locator).findFurtherRoots(resolvedRoot);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ public By useContent(@NonNull ContentType type) {
return this;
}

/**
* Returns the locator selected for the current content type.
*
* @return the locator selected by {@link #useContent(ContentType)}
*/
public By getCurrentLocator() {
return map.get(currentContent);
}

@Override
public WebElement findElement(SearchContext context) {
return context.findElement(map.get(currentContent));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ public ByAll(By... bys) {
this.bys = bys;
}

/**
* Returns the locators searched by this strategy, in order.
*
* @return a copy of the locators
*/
public By[] getBys() {
return bys.clone();
}

@Override
public WebElement findElement(SearchContext context) {
for (By by : bys) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ public ByChained(By... bys) {
this.bys = bys;
}

/**
* Returns the locators applied by this chain, in order.
*
* @return a copy of the locators
*/
public By[] getBys() {
return bys.clone();
}

@Override
public WebElement findElement(SearchContext context) {
List<WebElement> elements = findElements(context);
Expand Down
Loading
Loading