Skip to content

fix: Properly unwrap driver instance if the ContextAware object is deeply nested #2052

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 5 commits into from
Oct 22, 2023
Merged
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 @@ -19,7 +19,6 @@
import io.appium.java_client.internal.CapabilityHelpers;
import io.appium.java_client.pagefactory.bys.ContentType;
import io.appium.java_client.pagefactory.locator.CacheableLocator;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.HasCapabilities;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.WebDriver;
Expand All @@ -46,7 +45,7 @@
import java.util.Map;

import static io.appium.java_client.pagefactory.utils.ProxyFactory.getEnhancedProxy;
import static io.appium.java_client.pagefactory.utils.WebDriverUnpackUtility.unpackWebDriverFromSearchContext;
import static io.appium.java_client.pagefactory.utils.WebDriverUnpackUtility.unpackObjectFromSearchContext;
import static io.appium.java_client.remote.options.SupportsAutomationNameOption.AUTOMATION_NAME_OPTION;
import static java.time.Duration.ofSeconds;

Expand Down Expand Up @@ -82,23 +81,15 @@ public class AppiumFieldDecorator implements FieldDecorator {
* @param duration is a desired duration of the waiting for an element presence.
*/
public AppiumFieldDecorator(SearchContext context, Duration duration) {
WebDriver wd = unpackWebDriverFromSearchContext(context);
this.webDriverReference = wd == null ? null : new WeakReference<>(wd);
if (wd instanceof HasCapabilities) {
Capabilities caps = ((HasCapabilities) wd).getCapabilities();
this.platform = CapabilityHelpers.getCapability(caps, CapabilityType.PLATFORM_NAME, String.class);
this.automation = CapabilityHelpers.getCapability(caps, AUTOMATION_NAME_OPTION, String.class);
} else {
this.platform = null;
this.automation = null;
}

this.webDriverReference = unpackObjectFromSearchContext(context, WebDriver.class)
.map(WeakReference::new).orElse(null);
this.platform = readStringCapability(context, CapabilityType.PLATFORM_NAME);
this.automation = readStringCapability(context, AUTOMATION_NAME_OPTION);
this.duration = duration;

defaultElementFieldDecorator = createFieldDecorator(new AppiumElementLocatorFactory(
context, duration, new DefaultElementByBuilder(platform, automation)
));

widgetLocatorFactory = new AppiumElementLocatorFactory(
context, duration, new WidgetByBuilder(platform, automation)
);
Expand All @@ -117,28 +108,32 @@ public AppiumFieldDecorator(SearchContext context) {
* @param duration is a desired duration of the waiting for an element presence.
*/
AppiumFieldDecorator(WeakReference<SearchContext> contextReference, Duration duration) {
WebDriver wd = unpackWebDriverFromSearchContext(contextReference.get());
this.webDriverReference = wd == null ? null : new WeakReference<>(wd);
if (wd instanceof HasCapabilities) {
Capabilities caps = ((HasCapabilities) wd).getCapabilities();
this.platform = CapabilityHelpers.getCapability(caps, CapabilityType.PLATFORM_NAME, String.class);
this.automation = CapabilityHelpers.getCapability(caps, AUTOMATION_NAME_OPTION, String.class);
} else {
this.platform = null;
this.automation = null;
}

var cr = contextReference.get();
this.webDriverReference = unpackObjectFromSearchContext(cr, WebDriver.class)
.map(WeakReference::new).orElse(null);
this.platform = readStringCapability(cr, CapabilityType.PLATFORM_NAME);
this.automation = readStringCapability(cr, AUTOMATION_NAME_OPTION);
this.duration = duration;

defaultElementFieldDecorator = createFieldDecorator(new AppiumElementLocatorFactory(
contextReference, duration, new DefaultElementByBuilder(platform, automation)
));

widgetLocatorFactory = new AppiumElementLocatorFactory(
contextReference, duration, new WidgetByBuilder(platform, automation)
);
}

@Nullable
private String readStringCapability(SearchContext searchContext, String capName) {
if (searchContext == null) {
return null;
}
return unpackObjectFromSearchContext(searchContext, HasCapabilities.class)
.map(HasCapabilities::getCapabilities)
.map(caps -> CapabilityHelpers.getCapability(caps, capName, String.class))
.orElse(null);
}

private DefaultFieldDecorator createFieldDecorator(ElementLocatorFactory factory) {
return new DefaultFieldDecorator(factory) {
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@

import javax.annotation.Nullable;

import java.util.Optional;

import static io.appium.java_client.pagefactory.bys.ContentType.HTML_OR_DEFAULT;
import static io.appium.java_client.pagefactory.bys.ContentType.NATIVE_MOBILE_SPECIFIC;
import static java.util.Optional.ofNullable;

public final class WebDriverUnpackUtility {
private static final String NATIVE_APP_PATTERN = "NATIVE_APP";
Expand All @@ -37,35 +38,53 @@ private WebDriverUnpackUtility() {
}

/**
* This method extract an instance of {@link WebDriver} from the given {@link SearchContext}.
* This method extracts an instance of the given interface from the given {@link SearchContext}.
* It is expected that the {@link SearchContext} itself or the object it wraps implements it.
*
* @param searchContext is an instance of {@link SearchContext}. It may be the instance of
* {@link WebDriver} or {@link org.openqa.selenium.WebElement} or some other
* user's extension/implementation.
* Note: if you want to use your own implementation then it should implement
* {@link WrapsDriver} or {@link WrapsElement}
* @return the instance of {@link WebDriver}.
* Note: if the given {@link SearchContext} is not
* {@link WebDriver} and it doesn't implement
* {@link WrapsDriver} or {@link WrapsElement} then this method returns null.
*
* @param cls interface whose instance is going to be extracted.
* @return Either an instance of the given interface or Optional.empty().
*/
@Nullable
public static WebDriver unpackWebDriverFromSearchContext(SearchContext searchContext) {
if (searchContext instanceof WebDriver) {
return (WebDriver) searchContext;
public static <T> Optional<T> unpackObjectFromSearchContext(@Nullable SearchContext searchContext, Class<T> cls) {
if (searchContext == null) {
return Optional.empty();
}

if (cls.isAssignableFrom(searchContext.getClass())) {
return Optional.of(cls.cast(searchContext));
}
if (searchContext instanceof WrapsDriver) {
return unpackWebDriverFromSearchContext(((WrapsDriver) searchContext).getWrappedDriver());
return unpackObjectFromSearchContext(((WrapsDriver) searchContext).getWrappedDriver(), cls);
}

// Search context it is not only WebDriver. WebElement is search context too.
// RemoteWebElement implements WrapsDriver
if (searchContext instanceof WrapsElement) {
return unpackWebDriverFromSearchContext(((WrapsElement) searchContext).getWrappedElement());
return unpackObjectFromSearchContext(((WrapsElement) searchContext).getWrappedElement(), cls);
}

return null;
return Optional.empty();
}

/**
* This method extract an instance of {@link WebDriver} from the given {@link SearchContext}.
* @param searchContext is an instance of {@link SearchContext}. It may be the instance of
* {@link WebDriver} or {@link org.openqa.selenium.WebElement} or some other
* user's extension/implementation.
* Note: if you want to use your own implementation then it should implement
* {@link WrapsDriver} or {@link WrapsElement}
* @return the instance of {@link WebDriver}.
* Note: if the given {@link SearchContext} is not
* {@link WebDriver} and it doesn't implement
* {@link WrapsDriver} or {@link WrapsElement} then this method returns null.
*
*/
@Nullable
public static WebDriver unpackWebDriverFromSearchContext(@Nullable SearchContext searchContext) {
return unpackObjectFromSearchContext(searchContext, WebDriver.class).orElse(null);
}

/**
Expand All @@ -83,20 +102,17 @@ public static WebDriver unpackWebDriverFromSearchContext(SearchContext searchCon
* {@link SearchContext} instance doesn't implement {@link ContextAware} and {@link WrapsDriver}
*/
public static ContentType getCurrentContentType(SearchContext context) {
return ofNullable(unpackWebDriverFromSearchContext(context)).map(driver -> {
if (driver instanceof HasBrowserCheck && !((HasBrowserCheck) driver).isBrowser()) {
return NATIVE_MOBILE_SPECIFIC;
}
var browserCheckHolder = unpackObjectFromSearchContext(context, HasBrowserCheck.class);
if (browserCheckHolder.filter(hbc -> !hbc.isBrowser()).isPresent()) {
return NATIVE_MOBILE_SPECIFIC;
}

if (ContextAware.class.isAssignableFrom(driver.getClass())) { //it is desktop browser
ContextAware contextAware = (ContextAware) driver;
var currentContext = contextAware.getContext();
if (currentContext != null && currentContext.toUpperCase().contains(NATIVE_APP_PATTERN)) {
return NATIVE_MOBILE_SPECIFIC;
}
}
var contextAware = unpackObjectFromSearchContext(context, ContextAware.class);
if (contextAware.map(ContextAware::getContext)
.filter(c -> c.toUpperCase().contains(NATIVE_APP_PATTERN)).isPresent()) {
return NATIVE_MOBILE_SPECIFIC;
}

return HTML_OR_DEFAULT;
}).orElse(HTML_OR_DEFAULT);
return HTML_OR_DEFAULT;
}
}