From 86ccb9a5efbca2f2851b091bf146ce84d0242c69 Mon Sep 17 00:00:00 2001 From: dlowzzxx <210235699+dlowzzxx@users.noreply.github.com> Date: Fri, 25 Sep 2026 17:04:27 +0200 Subject: [PATCH] fix: proxy abstract widget methods for platform overrides --- .../pagefactory/utils/ProxyFactory.java | 3 - .../appium/java_client/proxy/Interceptor.java | 17 ++- .../combined/AbstractWidgetMethodTest.java | 100 ++++++++++++++++++ .../java_client/proxy/ProxyHelpersTest.java | 11 ++ 4 files changed, 123 insertions(+), 8 deletions(-) create mode 100644 src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/AbstractWidgetMethodTest.java diff --git a/src/main/java/io/appium/java_client/pagefactory/utils/ProxyFactory.java b/src/main/java/io/appium/java_client/pagefactory/utils/ProxyFactory.java index 9e33276e5..b0ee40c7e 100644 --- a/src/main/java/io/appium/java_client/pagefactory/utils/ProxyFactory.java +++ b/src/main/java/io/appium/java_client/pagefactory/utils/ProxyFactory.java @@ -28,7 +28,6 @@ import static io.appium.java_client.proxy.Helpers.OBJECT_METHOD_NAMES; import static io.appium.java_client.proxy.Helpers.createProxy; -import static net.bytebuddy.matcher.ElementMatchers.isAbstract; import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; import static net.bytebuddy.matcher.ElementMatchers.not; @@ -89,8 +88,6 @@ public static T getEnhancedProxy( ) { ElementMatcher extraMatcher = not( namedOneOf(NON_PROXYABLE_METHODS.toArray(new String[0])) - ).and( - not(isAbstract()) ); return createProxy( cls, diff --git a/src/main/java/io/appium/java_client/proxy/Interceptor.java b/src/main/java/io/appium/java_client/proxy/Interceptor.java index f4ece1668..7dd1c6eba 100644 --- a/src/main/java/io/appium/java_client/proxy/Interceptor.java +++ b/src/main/java/io/appium/java_client/proxy/Interceptor.java @@ -44,7 +44,9 @@ private Interceptor() { * @param self The reference to the original instance. * @param method The reference to the original method. * @param args The reference to method args. - * @param callable The reference to the non-patched callable to avoid call recursion. + * @param callable The reference to the non-patched callable to avoid call recursion. Abstract methods + * have no superclass implementation, so this may be null; unhandled calls retain + * their {@link AbstractMethodError} behavior. * @return Either the original method result or the patched one. */ @SuppressWarnings("unused") @@ -53,11 +55,16 @@ public static Object intercept( @This Object self, @Origin Method method, @AllArguments Object[] args, - @SuperCall Callable callable + @SuperCall(nullIfImpossible = true) Callable callable ) throws Throwable { + Callable original = callable == null + ? () -> { + throw new AbstractMethodError(method.toString()); + } + : callable; var listeners = ((HasMethodCallListeners) self).getMethodCallListeners(); if (listeners == null || listeners.length == 0) { - return callable.call(); + return original.call(); } for (var listener : listeners) { @@ -75,7 +82,7 @@ public static Object intercept( Object result = UNSET; for (var listener : listeners) { try { - result = listener.call(self, method, args, callable); + result = listener.call(self, method, args, original); if (result != UNSET) { break; } @@ -95,7 +102,7 @@ public static Object intercept( } if (UNSET == result) { try { - result = callable.call(); + result = original.call(); } catch (Exception e) { for (var listener : listeners) { try { diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/AbstractWidgetMethodTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/AbstractWidgetMethodTest.java new file mode 100644 index 000000000..acd1899a4 --- /dev/null +++ b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/AbstractWidgetMethodTest.java @@ -0,0 +1,100 @@ +package io.appium.java_client.pagefactory_tests.widget.tests.combined; + +import io.appium.java_client.pagefactory.AndroidFindBy; +import io.appium.java_client.pagefactory.AppiumFieldDecorator; +import io.appium.java_client.pagefactory.OverrideWidget; +import io.appium.java_client.pagefactory.Widget; +import io.appium.java_client.pagefactory.iOSXCUITFindBy; +import io.appium.java_client.pagefactory_tests.widget.tests.AbstractStubWebDriver; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.openqa.selenium.support.PageFactory.initElements; + +class AbstractWidgetMethodTest { + + static Stream drivers() { + return Stream.of( + Arguments.of(new AbstractStubWebDriver.StubAndroidDriver(), "android"), + Arguments.of(new AbstractStubWebDriver.StubIOSXCUITDriver(), "ios"), + Arguments.of(new AbstractStubWebDriver.StubBrowserDriver(), "html"), + Arguments.of(new AbstractStubWebDriver.StubAndroidBrowserOrWebViewDriver(), "html") + ); + } + + @ParameterizedTest + @MethodSource("drivers") + void delegatesAbstractMethodToPlatformSpecificWidget(WebDriver driver, String expected) { + var page = new Page(); + initElements(new AppiumFieldDecorator(driver), page); + + assertEquals(expected, page.widget.platformAction()); + assertEquals(2, page.widgets.size()); + page.widgets.forEach(widget -> assertEquals(expected, widget.platformAction())); + } + + static class Page { + @OverrideWidget(html = HtmlWidget.class, + androidUIAutomator = AndroidWidget.class, + iOSXCUITAutomation = IosWidget.class) + @FindBy(id = "widget") + private AbstractActionWidget widget; + + @OverrideWidget(html = HtmlWidget.class, + androidUIAutomator = AndroidWidget.class, + iOSXCUITAutomation = IosWidget.class) + @FindBy(id = "widget") + private List widgets; + } + + public abstract static class AbstractActionWidget extends Widget { + protected AbstractActionWidget(WebElement element) { + super(element); + } + + public abstract String platformAction(); + } + + @AndroidFindBy(uiAutomator = "widget") + public static class AndroidWidget extends AbstractActionWidget { + protected AndroidWidget(WebElement element) { + super(element); + } + + @Override + public String platformAction() { + return "android"; + } + } + + @iOSXCUITFindBy(iOSNsPredicate = "widget") + public static class IosWidget extends AbstractActionWidget { + protected IosWidget(WebElement element) { + super(element); + } + + @Override + public String platformAction() { + return "ios"; + } + } + + public static class HtmlWidget extends AbstractActionWidget { + protected HtmlWidget(WebElement element) { + super(element); + } + + @Override + public String platformAction() { + return "html"; + } + } +} diff --git a/src/test/java/io/appium/java_client/proxy/ProxyHelpersTest.java b/src/test/java/io/appium/java_client/proxy/ProxyHelpersTest.java index af0ca78d9..ae1b280a1 100644 --- a/src/test/java/io/appium/java_client/proxy/ProxyHelpersTest.java +++ b/src/test/java/io/appium/java_client/proxy/ProxyHelpersTest.java @@ -35,6 +35,7 @@ import java.util.List; import java.util.concurrent.Callable; +import static io.appium.java_client.pagefactory.utils.ProxyFactory.getEnhancedProxy; import static io.appium.java_client.proxy.Helpers.createProxy; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; @@ -43,6 +44,16 @@ class ProxyHelpersTest { + public abstract static class AbstractAction { + public abstract String run(); + } + + @Test + void unhandledAbstractMethodRetainsAbstractMethodError() { + AbstractAction proxy = getEnhancedProxy(AbstractAction.class, new MethodCallListener() { }); + assertThrows(AbstractMethodError.class, proxy::run); + } + public static class FakeIOSDriver extends IOSDriver { public FakeIOSDriver(URL url, Capabilities caps) { super(url, caps);