From a79df4bfa879298756e51b14f6790431c5aecb46 Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Tue, 1 Jul 2025 19:23:53 +0530 Subject: [PATCH 01/13] fix: Proxy commands issues via RemoteWebElement Fixes #2239 --- .../io/appium/java_client/proxy/Helpers.java | 32 ++++++-- .../appium/java_client/proxy/Interceptor.java | 22 ++++++ .../java_client/proxy/ProxyHelpersTest.java | 79 +++++++++++++++++++ 3 files changed, 128 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/appium/java_client/proxy/Helpers.java b/src/main/java/io/appium/java_client/proxy/Helpers.java index f9fae7768..a280e660c 100644 --- a/src/main/java/io/appium/java_client/proxy/Helpers.java +++ b/src/main/java/io/appium/java_client/proxy/Helpers.java @@ -27,13 +27,11 @@ import net.bytebuddy.matcher.ElementMatcher; import net.bytebuddy.matcher.ElementMatchers; import org.jspecify.annotations.Nullable; +import org.openqa.selenium.remote.RemoteWebDriver; +import org.openqa.selenium.remote.RemoteWebElement; import java.lang.reflect.Method; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.Set; -import java.util.WeakHashMap; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -222,4 +220,28 @@ private static class ProxyClassSignature { Class[] constructorArgTypes; ElementMatcher extraMethodMatcher; } + + public static RemoteWebElement wrapElement( + RemoteWebElement original, + HasMethodCallListeners parent, + MethodCallListener[] listeners + ) { + RemoteWebElement proxy = createProxy( + RemoteWebElement.class, + new Object[]{}, + new Class[]{}, + List.of(listeners), + ElementMatchers.not( + namedOneOf( + OBJECT_METHOD_NAMES.toArray(new String[0])) + .or(ElementMatchers.named("setId").or(ElementMatchers.named("setParent"))) + ) + ); + + proxy.setId(original.getId()); + + proxy.setParent((RemoteWebDriver) parent); + + return proxy; + } } 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..eefab3a59 100644 --- a/src/main/java/io/appium/java_client/proxy/Interceptor.java +++ b/src/main/java/io/appium/java_client/proxy/Interceptor.java @@ -21,10 +21,13 @@ import net.bytebuddy.implementation.bind.annotation.RuntimeType; import net.bytebuddy.implementation.bind.annotation.SuperCall; import net.bytebuddy.implementation.bind.annotation.This; +import org.openqa.selenium.remote.RemoteWebElement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.Callable; import static io.appium.java_client.proxy.MethodCallListener.UNSET; @@ -111,6 +114,25 @@ public static Object intercept( } } + if (result instanceof RemoteWebElement) { + result = Helpers.wrapElement((RemoteWebElement) result, (HasMethodCallListeners) self, listeners); + } else if (result instanceof List) { + List originalList = (List) result; + if (!originalList.isEmpty() && originalList.get(0) instanceof RemoteWebElement) { + List wrappedList = new ArrayList<>(originalList.size()); + for (Object item : originalList) { + if (item instanceof RemoteWebElement) { + wrappedList.add(Helpers.wrapElement( + (RemoteWebElement) item, + (HasMethodCallListeners) self, listeners)); + } else { + wrappedList.add(item); + } + } + result = wrappedList; + } + } + final Object endResult = result == UNSET ? null : result; for (var listener : listeners) { try { 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 a8767629d..f060d3b51 100644 --- a/src/test/java/io/appium/java_client/proxy/ProxyHelpersTest.java +++ b/src/test/java/io/appium/java_client/proxy/ProxyHelpersTest.java @@ -19,14 +19,20 @@ import io.appium.java_client.ios.IOSDriver; import io.appium.java_client.ios.options.XCUITestOptions; import org.junit.jupiter.api.Test; +import org.openqa.selenium.By; import org.openqa.selenium.Capabilities; +import org.openqa.selenium.NoSuchSessionException; +import org.openqa.selenium.WebElement; import org.openqa.selenium.remote.RemoteWebDriver; +import org.openqa.selenium.remote.RemoteWebElement; import org.openqa.selenium.remote.UnreachableBrowserException; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.concurrent.Callable; import static io.appium.java_client.proxy.Helpers.createProxy; @@ -45,6 +51,31 @@ public FakeIOSDriver(URL url, Capabilities caps) { @Override protected void startSession(Capabilities capabilities) { } + + @Override + public WebElement findElement(By locator) { + RemoteWebElement webElement = new RemoteWebElement(); + webElement.setId(locator.toString()); + webElement.setParent(this); + return webElement; + } + + @Override + public List findElements(By locator) { + List webElements = new ArrayList<>(); + + RemoteWebElement webElement1 = new RemoteWebElement(); + webElement1.setId("1234"); + webElement1.setParent(this); + webElements.add(webElement1); + + RemoteWebElement webElement2 = new RemoteWebElement(); + webElement2.setId("5678"); + webElement2.setParent(this); + webElements.add(webElement2); + + return webElements; + } } @Test @@ -133,4 +164,52 @@ public Object onError(Object obj, Method method, Object[] args, Throwable e) thr "onError get") ))); } + + + @Test + void shouldFireEventsForRemoteWebElement() throws MalformedURLException { + final StringBuilder acc = new StringBuilder(); + MethodCallListener listener = new MethodCallListener() { + @Override + public void beforeCall(Object target, Method method, Object[] args) { + acc.append("beforeCall ").append(method.getName()).append("\n"); + } + }; + + FakeIOSDriver driver = createProxy( + FakeIOSDriver.class, + new Object[] {new URL("http://localhost:4723/"), new XCUITestOptions()}, + new Class[] {URL.class, Capabilities.class}, + listener + ); + + WebElement element = driver.findElement(By.id("button")); + + assertThrows( + NoSuchSessionException.class, + element::click + ); + + List elements = driver.findElements(By.id("button")); + + assertThrows( + NoSuchSessionException.class, + () -> elements.get(1).isSelected() + ); + + assertThat(acc.toString().trim(), is(equalTo( + String.join("\n", + "beforeCall findElement", + "beforeCall click", + "beforeCall getSessionId", + "beforeCall getCapabilities", + "beforeCall getCapabilities", + "beforeCall findElements", + "beforeCall isSelected", + "beforeCall getSessionId", + "beforeCall getCapabilities", + "beforeCall getCapabilities" + ) + ))); + } } From 5f657cc663add08032aeb96ad122c6e310fe1913 Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Wed, 2 Jul 2025 10:20:25 +0530 Subject: [PATCH 02/13] Address comments --- .../io/appium/java_client/proxy/Helpers.java | 7 +++- .../appium/java_client/proxy/Interceptor.java | 32 +++++++++++-------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/main/java/io/appium/java_client/proxy/Helpers.java b/src/main/java/io/appium/java_client/proxy/Helpers.java index a280e660c..24c32a07f 100644 --- a/src/main/java/io/appium/java_client/proxy/Helpers.java +++ b/src/main/java/io/appium/java_client/proxy/Helpers.java @@ -31,7 +31,12 @@ import org.openqa.selenium.remote.RemoteWebElement; import java.lang.reflect.Method; -import java.util.*; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; import java.util.stream.Collectors; import java.util.stream.Stream; 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 eefab3a59..7747b2eeb 100644 --- a/src/main/java/io/appium/java_client/proxy/Interceptor.java +++ b/src/main/java/io/appium/java_client/proxy/Interceptor.java @@ -26,9 +26,9 @@ import org.slf4j.LoggerFactory; import java.lang.reflect.Method; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; +import java.util.stream.Collectors; import static io.appium.java_client.proxy.MethodCallListener.UNSET; @@ -115,21 +115,25 @@ public static Object intercept( } if (result instanceof RemoteWebElement) { - result = Helpers.wrapElement((RemoteWebElement) result, (HasMethodCallListeners) self, listeners); + result = Helpers.wrapElement( + (RemoteWebElement) result, + (HasMethodCallListeners) self, + listeners); } else if (result instanceof List) { List originalList = (List) result; - if (!originalList.isEmpty() && originalList.get(0) instanceof RemoteWebElement) { - List wrappedList = new ArrayList<>(originalList.size()); - for (Object item : originalList) { - if (item instanceof RemoteWebElement) { - wrappedList.add(Helpers.wrapElement( - (RemoteWebElement) item, - (HasMethodCallListeners) self, listeners)); - } else { - wrappedList.add(item); - } - } - result = wrappedList; + + if (!originalList.isEmpty()) { + result = originalList.stream() + .map(item -> { + if (item instanceof RemoteWebElement) { + return Helpers.wrapElement( + (RemoteWebElement) item, + (HasMethodCallListeners) self, + listeners); + } + return item; + }) + .collect(Collectors.toList()); } } From 7aa6bfe689e7d8e240b66d2fe8f69b1c1c170d00 Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Wed, 2 Jul 2025 13:00:36 +0530 Subject: [PATCH 03/13] Modularize into RemoteWebElementListener --- .../io/appium/java_client/proxy/Helpers.java | 32 +----- .../appium/java_client/proxy/Interceptor.java | 23 ---- .../java_client/proxy/ProxyAwareListener.java | 22 ++++ .../proxy/RemoteWebElementListener.java | 108 ++++++++++++++++++ .../java_client/proxy/ProxyHelpersTest.java | 4 +- 5 files changed, 138 insertions(+), 51 deletions(-) create mode 100644 src/main/java/io/appium/java_client/proxy/ProxyAwareListener.java create mode 100644 src/main/java/io/appium/java_client/proxy/RemoteWebElementListener.java diff --git a/src/main/java/io/appium/java_client/proxy/Helpers.java b/src/main/java/io/appium/java_client/proxy/Helpers.java index 24c32a07f..880c45e7e 100644 --- a/src/main/java/io/appium/java_client/proxy/Helpers.java +++ b/src/main/java/io/appium/java_client/proxy/Helpers.java @@ -27,13 +27,10 @@ import net.bytebuddy.matcher.ElementMatcher; import net.bytebuddy.matcher.ElementMatchers; import org.jspecify.annotations.Nullable; -import org.openqa.selenium.remote.RemoteWebDriver; -import org.openqa.selenium.remote.RemoteWebElement; import java.lang.reflect.Method; import java.util.Collection; import java.util.Collections; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; @@ -148,6 +145,11 @@ public static T createProxy( try { T result = cls.cast(proxyClass.getConstructor(constructorArgTypes).newInstance(constructorArgs)); ((HasMethodCallListeners) result).setMethodCallListeners(listeners.toArray(MethodCallListener[]::new)); + + listeners.stream() + .filter(ProxyAwareListener.class::isInstance) + .forEach(listener -> ((ProxyAwareListener) listener).attachProxyInstance(result)); + return result; } catch (SecurityException | ReflectiveOperationException e) { throw new IllegalStateException(String.format("Unable to create a proxy of %s", cls.getName()), e); @@ -225,28 +227,4 @@ private static class ProxyClassSignature { Class[] constructorArgTypes; ElementMatcher extraMethodMatcher; } - - public static RemoteWebElement wrapElement( - RemoteWebElement original, - HasMethodCallListeners parent, - MethodCallListener[] listeners - ) { - RemoteWebElement proxy = createProxy( - RemoteWebElement.class, - new Object[]{}, - new Class[]{}, - List.of(listeners), - ElementMatchers.not( - namedOneOf( - OBJECT_METHOD_NAMES.toArray(new String[0])) - .or(ElementMatchers.named("setId").or(ElementMatchers.named("setParent"))) - ) - ); - - proxy.setId(original.getId()); - - proxy.setParent((RemoteWebDriver) parent); - - return proxy; - } } 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 7747b2eeb..de209cf4f 100644 --- a/src/main/java/io/appium/java_client/proxy/Interceptor.java +++ b/src/main/java/io/appium/java_client/proxy/Interceptor.java @@ -114,29 +114,6 @@ public static Object intercept( } } - if (result instanceof RemoteWebElement) { - result = Helpers.wrapElement( - (RemoteWebElement) result, - (HasMethodCallListeners) self, - listeners); - } else if (result instanceof List) { - List originalList = (List) result; - - if (!originalList.isEmpty()) { - result = originalList.stream() - .map(item -> { - if (item instanceof RemoteWebElement) { - return Helpers.wrapElement( - (RemoteWebElement) item, - (HasMethodCallListeners) self, - listeners); - } - return item; - }) - .collect(Collectors.toList()); - } - } - final Object endResult = result == UNSET ? null : result; for (var listener : listeners) { try { diff --git a/src/main/java/io/appium/java_client/proxy/ProxyAwareListener.java b/src/main/java/io/appium/java_client/proxy/ProxyAwareListener.java new file mode 100644 index 000000000..936f3eee8 --- /dev/null +++ b/src/main/java/io/appium/java_client/proxy/ProxyAwareListener.java @@ -0,0 +1,22 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.proxy; + +public interface ProxyAwareListener extends MethodCallListener { + void attachProxyInstance(Object proxy); +} + diff --git a/src/main/java/io/appium/java_client/proxy/RemoteWebElementListener.java b/src/main/java/io/appium/java_client/proxy/RemoteWebElementListener.java new file mode 100644 index 000000000..1e8223662 --- /dev/null +++ b/src/main/java/io/appium/java_client/proxy/RemoteWebElementListener.java @@ -0,0 +1,108 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.proxy; + +import com.google.common.base.Preconditions; +import net.bytebuddy.matcher.ElementMatchers; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.remote.RemoteWebDriver; +import org.openqa.selenium.remote.RemoteWebElement; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.stream.Collectors; + +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.namedOneOf; + +public class RemoteWebElementListener implements MethodCallListener, ProxyAwareListener { + private WebDriver parent; + private final Collection listeners; + + public RemoteWebElementListener(MethodCallListener listener) { + this.listeners = Collections.singletonList(listener); + } + + public RemoteWebElementListener(Collection listeners) { + this.listeners = listeners; + } + + @Override + public void attachProxyInstance(Object proxy) { + Preconditions.checkArgument(proxy instanceof WebDriver); + this.parent = (WebDriver) proxy; + } + + @Override + public Object call(Object obj, Method method, Object[] args, Callable original) throws Throwable { + Object result = original.call(); + + if (result instanceof RemoteWebElement) { + result = wrapElement( + (RemoteWebElement) result, + parent, + listeners); + } else if (result instanceof List) { + List originalList = (List) result; + + if (!originalList.isEmpty()) { + result = originalList.stream() + .map(item -> { + if (item instanceof RemoteWebElement) { + return wrapElement( + (RemoteWebElement) item, + parent, + listeners); + } + return item; + }) + .collect(Collectors.toList()); + } + } + + return result; + } + + private RemoteWebElement wrapElement( + RemoteWebElement original, + WebDriver parent, + Collection listeners + ) { + RemoteWebElement proxy = createProxy( + RemoteWebElement.class, + new Object[]{}, + new Class[]{}, + listeners, + ElementMatchers.not( + namedOneOf( + OBJECT_METHOD_NAMES.toArray(new String[0])) + .or(ElementMatchers.named("setId").or(ElementMatchers.named("setParent"))) + ) + ); + + proxy.setId(original.getId()); + + proxy.setParent((RemoteWebDriver) parent); + + return proxy; + } + +} 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 f060d3b51..2b8712686 100644 --- a/src/test/java/io/appium/java_client/proxy/ProxyHelpersTest.java +++ b/src/test/java/io/appium/java_client/proxy/ProxyHelpersTest.java @@ -176,11 +176,13 @@ public void beforeCall(Object target, Method method, Object[] args) { } }; + RemoteWebElementListener remoteWebElementListener = new RemoteWebElementListener(listener); + FakeIOSDriver driver = createProxy( FakeIOSDriver.class, new Object[] {new URL("http://localhost:4723/"), new XCUITestOptions()}, new Class[] {URL.class, Capabilities.class}, - listener + List.of(remoteWebElementListener, listener) ); WebElement element = driver.findElement(By.id("button")); From c2feeff17cdaf383449ffb52ad06c455e04f770e Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Wed, 2 Jul 2025 13:01:00 +0530 Subject: [PATCH 04/13] Remove unused imports --- src/main/java/io/appium/java_client/proxy/Interceptor.java | 3 --- 1 file changed, 3 deletions(-) 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 de209cf4f..f4ece1668 100644 --- a/src/main/java/io/appium/java_client/proxy/Interceptor.java +++ b/src/main/java/io/appium/java_client/proxy/Interceptor.java @@ -21,14 +21,11 @@ import net.bytebuddy.implementation.bind.annotation.RuntimeType; import net.bytebuddy.implementation.bind.annotation.SuperCall; import net.bytebuddy.implementation.bind.annotation.This; -import org.openqa.selenium.remote.RemoteWebElement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Method; -import java.util.List; import java.util.concurrent.Callable; -import java.util.stream.Collectors; import static io.appium.java_client.proxy.MethodCallListener.UNSET; From 68cfbaa0a31ecd5e06e6920281c59d437a0eab22 Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Wed, 2 Jul 2025 13:37:20 +0530 Subject: [PATCH 05/13] Address comments --- .../proxy/RemoteWebElementListener.java | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/main/java/io/appium/java_client/proxy/RemoteWebElementListener.java b/src/main/java/io/appium/java_client/proxy/RemoteWebElementListener.java index 1e8223662..7c188a4f7 100644 --- a/src/main/java/io/appium/java_client/proxy/RemoteWebElementListener.java +++ b/src/main/java/io/appium/java_client/proxy/RemoteWebElementListener.java @@ -56,26 +56,24 @@ public Object call(Object obj, Method method, Object[] args, Callable origina Object result = original.call(); if (result instanceof RemoteWebElement) { - result = wrapElement( + return wrapElement( (RemoteWebElement) result, parent, listeners); - } else if (result instanceof List) { - List originalList = (List) result; - - if (!originalList.isEmpty()) { - result = originalList.stream() - .map(item -> { - if (item instanceof RemoteWebElement) { - return wrapElement( - (RemoteWebElement) item, - parent, - listeners); - } - return item; - }) - .collect(Collectors.toList()); - } + } + + if (result instanceof List) { + return ((List) result).stream() + .map(item -> { + if (item instanceof RemoteWebElement) { + return wrapElement( + (RemoteWebElement) item, + parent, + listeners); + } + return item; + }) + .collect(Collectors.toList()); } return result; From 2b93de81a473447b621856e094ed15cbcfa548c7 Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Wed, 2 Jul 2025 14:44:34 +0530 Subject: [PATCH 06/13] Address comments --- .../java_client/proxy/RemoteWebElementListener.java | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/main/java/io/appium/java_client/proxy/RemoteWebElementListener.java b/src/main/java/io/appium/java_client/proxy/RemoteWebElementListener.java index 7c188a4f7..6e12736f0 100644 --- a/src/main/java/io/appium/java_client/proxy/RemoteWebElementListener.java +++ b/src/main/java/io/appium/java_client/proxy/RemoteWebElementListener.java @@ -64,15 +64,8 @@ public Object call(Object obj, Method method, Object[] args, Callable origina if (result instanceof List) { return ((List) result).stream() - .map(item -> { - if (item instanceof RemoteWebElement) { - return wrapElement( - (RemoteWebElement) item, - parent, - listeners); - } - return item; - }) + .map(item -> item instanceof RemoteWebElement ? wrapElement( + (RemoteWebElement) item, parent, listeners) : item) .collect(Collectors.toList()); } From 458259f1bf3a2d4dabd9de080b685536945f7119 Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Wed, 2 Jul 2025 15:53:48 +0530 Subject: [PATCH 07/13] Address comments --- ...ntListener.java => WebDriverListener.java} | 26 ++++++------------- .../java_client/proxy/ProxyHelpersTest.java | 9 +++---- 2 files changed, 12 insertions(+), 23 deletions(-) rename src/main/java/io/appium/java_client/proxy/{RemoteWebElementListener.java => WebDriverListener.java} (78%) diff --git a/src/main/java/io/appium/java_client/proxy/RemoteWebElementListener.java b/src/main/java/io/appium/java_client/proxy/WebDriverListener.java similarity index 78% rename from src/main/java/io/appium/java_client/proxy/RemoteWebElementListener.java rename to src/main/java/io/appium/java_client/proxy/WebDriverListener.java index 6e12736f0..02c798b56 100644 --- a/src/main/java/io/appium/java_client/proxy/RemoteWebElementListener.java +++ b/src/main/java/io/appium/java_client/proxy/WebDriverListener.java @@ -16,14 +16,12 @@ package io.appium.java_client.proxy; -import com.google.common.base.Preconditions; import net.bytebuddy.matcher.ElementMatchers; import org.openqa.selenium.WebDriver; import org.openqa.selenium.remote.RemoteWebDriver; import org.openqa.selenium.remote.RemoteWebElement; import java.lang.reflect.Method; -import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.concurrent.Callable; @@ -33,22 +31,14 @@ import static io.appium.java_client.proxy.Helpers.createProxy; import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; -public class RemoteWebElementListener implements MethodCallListener, ProxyAwareListener { +public class WebDriverListener implements MethodCallListener, ProxyAwareListener { private WebDriver parent; - private final Collection listeners; - - public RemoteWebElementListener(MethodCallListener listener) { - this.listeners = Collections.singletonList(listener); - } - - public RemoteWebElementListener(Collection listeners) { - this.listeners = listeners; - } @Override public void attachProxyInstance(Object proxy) { - Preconditions.checkArgument(proxy instanceof WebDriver); - this.parent = (WebDriver) proxy; + if (proxy instanceof WebDriver) { + this.parent = (WebDriver) proxy; + } } @Override @@ -59,13 +49,13 @@ public Object call(Object obj, Method method, Object[] args, Callable origina return wrapElement( (RemoteWebElement) result, parent, - listeners); + this); } if (result instanceof List) { return ((List) result).stream() .map(item -> item instanceof RemoteWebElement ? wrapElement( - (RemoteWebElement) item, parent, listeners) : item) + (RemoteWebElement) item, parent, this) : item) .collect(Collectors.toList()); } @@ -75,13 +65,13 @@ public Object call(Object obj, Method method, Object[] args, Callable origina private RemoteWebElement wrapElement( RemoteWebElement original, WebDriver parent, - Collection listeners + MethodCallListener listener ) { RemoteWebElement proxy = createProxy( RemoteWebElement.class, new Object[]{}, new Class[]{}, - listeners, + Collections.singletonList(listener), ElementMatchers.not( namedOneOf( OBJECT_METHOD_NAMES.toArray(new String[0])) 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 2b8712686..2eda4fce2 100644 --- a/src/test/java/io/appium/java_client/proxy/ProxyHelpersTest.java +++ b/src/test/java/io/appium/java_client/proxy/ProxyHelpersTest.java @@ -167,22 +167,21 @@ public Object onError(Object obj, Method method, Object[] args, Throwable e) thr @Test - void shouldFireEventsForRemoteWebElement() throws MalformedURLException { + void shouldFireEventsForAllWebDriverCommands() throws MalformedURLException { final StringBuilder acc = new StringBuilder(); - MethodCallListener listener = new MethodCallListener() { + + WebDriverListener remoteWebElementListener = new WebDriverListener() { @Override public void beforeCall(Object target, Method method, Object[] args) { acc.append("beforeCall ").append(method.getName()).append("\n"); } }; - RemoteWebElementListener remoteWebElementListener = new RemoteWebElementListener(listener); - FakeIOSDriver driver = createProxy( FakeIOSDriver.class, new Object[] {new URL("http://localhost:4723/"), new XCUITestOptions()}, new Class[] {URL.class, Capabilities.class}, - List.of(remoteWebElementListener, listener) + remoteWebElementListener ); WebElement element = driver.findElement(By.id("button")); From 5e6fabe34052c3c56cc7116de6cb7f198d70333d Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Wed, 2 Jul 2025 16:24:30 +0530 Subject: [PATCH 08/13] Rename the new listener --- ...ebDriverListener.java => ElementAwareWebDriverListener.java} | 2 +- src/test/java/io/appium/java_client/proxy/ProxyHelpersTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/main/java/io/appium/java_client/proxy/{WebDriverListener.java => ElementAwareWebDriverListener.java} (96%) diff --git a/src/main/java/io/appium/java_client/proxy/WebDriverListener.java b/src/main/java/io/appium/java_client/proxy/ElementAwareWebDriverListener.java similarity index 96% rename from src/main/java/io/appium/java_client/proxy/WebDriverListener.java rename to src/main/java/io/appium/java_client/proxy/ElementAwareWebDriverListener.java index 02c798b56..63a65873b 100644 --- a/src/main/java/io/appium/java_client/proxy/WebDriverListener.java +++ b/src/main/java/io/appium/java_client/proxy/ElementAwareWebDriverListener.java @@ -31,7 +31,7 @@ import static io.appium.java_client.proxy.Helpers.createProxy; import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; -public class WebDriverListener implements MethodCallListener, ProxyAwareListener { +public class ElementAwareWebDriverListener implements MethodCallListener, ProxyAwareListener { private WebDriver parent; @Override 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 2eda4fce2..33d61d4fa 100644 --- a/src/test/java/io/appium/java_client/proxy/ProxyHelpersTest.java +++ b/src/test/java/io/appium/java_client/proxy/ProxyHelpersTest.java @@ -170,7 +170,7 @@ public Object onError(Object obj, Method method, Object[] args, Throwable e) thr void shouldFireEventsForAllWebDriverCommands() throws MalformedURLException { final StringBuilder acc = new StringBuilder(); - WebDriverListener remoteWebElementListener = new WebDriverListener() { + ElementAwareWebDriverListener remoteWebElementListener = new ElementAwareWebDriverListener() { @Override public void beforeCall(Object target, Method method, Object[] args) { acc.append("beforeCall ").append(method.getName()).append("\n"); From b33ebaeb3f110f237fe875424504eda3d8cae44a Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Wed, 2 Jul 2025 16:25:13 +0530 Subject: [PATCH 09/13] Address comments --- src/test/java/io/appium/java_client/proxy/ProxyHelpersTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 33d61d4fa..af0ca78d9 100644 --- a/src/test/java/io/appium/java_client/proxy/ProxyHelpersTest.java +++ b/src/test/java/io/appium/java_client/proxy/ProxyHelpersTest.java @@ -170,7 +170,7 @@ public Object onError(Object obj, Method method, Object[] args, Throwable e) thr void shouldFireEventsForAllWebDriverCommands() throws MalformedURLException { final StringBuilder acc = new StringBuilder(); - ElementAwareWebDriverListener remoteWebElementListener = new ElementAwareWebDriverListener() { + var remoteWebElementListener = new ElementAwareWebDriverListener() { @Override public void beforeCall(Object target, Method method, Object[] args) { acc.append("beforeCall ").append(method.getName()).append("\n"); From f3797578dc832431fbbd3bab663a1ec254366979 Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Wed, 2 Jul 2025 16:55:42 +0530 Subject: [PATCH 10/13] Add comments describing public methods --- .../proxy/ElementAwareWebDriverListener.java | 23 +++++++++++++++++++ .../java_client/proxy/ProxyAwareListener.java | 19 +++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/main/java/io/appium/java_client/proxy/ElementAwareWebDriverListener.java b/src/main/java/io/appium/java_client/proxy/ElementAwareWebDriverListener.java index 63a65873b..76e529dee 100644 --- a/src/main/java/io/appium/java_client/proxy/ElementAwareWebDriverListener.java +++ b/src/main/java/io/appium/java_client/proxy/ElementAwareWebDriverListener.java @@ -34,6 +34,13 @@ public class ElementAwareWebDriverListener implements MethodCallListener, ProxyAwareListener { private WebDriver parent; + /** + * Attaches the WebDriver proxy instance to this listener. + *

+ * The listener stores the WebDriver instance to associate it as parent to RemoteWebElement proxies. + * + * @param proxy A proxy instance of {@link WebDriver}. + */ @Override public void attachProxyInstance(Object proxy) { if (proxy instanceof WebDriver) { @@ -41,6 +48,22 @@ public void attachProxyInstance(Object proxy) { } } + /** + * Intercepts method calls on a proxied WebDriver. + *

+ * If the result of the method call is a {@link RemoteWebElement}, + * it is wrapped with a proxy to allow further interception of RemoteWebElement method calls. + * If the result is a list, each item is checked, and all RemoteWebElements are + * individually proxied. All other return types are passed through unmodified. + * Avoid overriding this method, it will alter the behaviour of the listener. + * + * @param obj The object on which the method was invoked. + * @param method The method being invoked. + * @param args The arguments passed to the method. + * @param original A {@link Callable} that represents the original method execution. + * @return The (possibly wrapped) result of the method call. + * @throws Throwable if the original method or any wrapping logic throws an exception. + */ @Override public Object call(Object obj, Method method, Object[] args, Callable original) throws Throwable { Object result = original.call(); diff --git a/src/main/java/io/appium/java_client/proxy/ProxyAwareListener.java b/src/main/java/io/appium/java_client/proxy/ProxyAwareListener.java index 936f3eee8..f25c48a79 100644 --- a/src/main/java/io/appium/java_client/proxy/ProxyAwareListener.java +++ b/src/main/java/io/appium/java_client/proxy/ProxyAwareListener.java @@ -16,7 +16,26 @@ package io.appium.java_client.proxy; +/** + * Extension of {@link MethodCallListener} that allows access to the proxy instance it depends on. + *

+ * This interface is intended for listeners that need a reference to the proxy object. + *

+ * The {@link #attachProxyInstance(Object)} method will be invoked immediately after the proxy is created, + * allowing the listener to bind to it before any method interception begins. + *

+ * Example usage: Working with elements such as + * {@code RemoteWebElement} that require runtime mutation (e.g. setting parent driver or element ID). + */ public interface ProxyAwareListener extends MethodCallListener { + + /** + * Binds the listener to the proxy instance passed. + *

+ * This is called once, immediately after proxy creation and before the proxy is returned to the caller. + * + * @param proxy the proxy instance created via {@code createProxy} that this listener is attached to. + */ void attachProxyInstance(Object proxy); } From c2629744d321778f30fbcd2cb920963ec2a5e78c Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Wed, 2 Jul 2025 17:20:14 +0530 Subject: [PATCH 11/13] Update the docs - The-event_firing.md --- docs/The-event_firing.md | 57 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/docs/The-event_firing.md b/docs/The-event_firing.md index 527fddeb8..ff77c1247 100644 --- a/docs/The-event_firing.md +++ b/docs/The-event_firing.md @@ -154,3 +154,60 @@ This proxy is not tied to WebDriver descendants and could be used to any classes change/replace the original methods behavior. It is important to know that callbacks are **not** invoked for methods derived from the standard `Object` class, like `toString` or `equals`. Check [unit tests](../src/test/java/io/appium/java_client/proxy/ProxyHelpersTest.java) for more examples. + +#### ElementAwareWebDriverListener + +A specialized MethodCallListener that listens to all method calls on a WebDriver instance and automatically wraps any returned RemoteWebElement (or list of elements) with a proxy. This enables your listener to intercept and react to method calls on both: + +- The driver itself (e.g., findElement, getTitle) + +- Any elements returned by the driver (e.g., click, isSelected on a WebElement) + +```java +import io.appium.java_client.ios.IOSDriver; +import io.appium.java_client.ios.options.XCUITestOptions; +import io.appium.java_client.proxy.ElementAwareWebDriverListener; +import io.appium.java_client.proxy.Helpers; +import io.appium.java_client.proxy.MethodCallListener; + + +// ... + +final StringBuilder acc = new StringBuilder(); + +var listener = new ElementAwareWebDriverListener() { + @Override + public void beforeCall(Object target, Method method, Object[] args) { + acc.append("beforeCall ").append(method.getName()).append("\n"); + } +}; + +IOSDriver decoratedDriver = createProxy( + IOSDriver.class, + new Object[]{new URL("http://localhost:4723/"), new XCUITestOptions()}, + new Class[]{URL.class, Capabilities.class}, + listener +); + +WebElement element = decoratedDriver.findElement(By.id("button")); +element::click; + +List elements = decoratedDriver.findElements(By.id("button")); +elements.get(1).isSelected(); + +assertThat(acc.toString().trim()).isEqualTo( + String.join("\n", + "beforeCall findElement", + "beforeCall click", + "beforeCall getSessionId", + "beforeCall getCapabilities", + "beforeCall getCapabilities", + "beforeCall findElements", + "beforeCall isSelected", + "beforeCall getSessionId", + "beforeCall getCapabilities", + "beforeCall getCapabilities" + ) +); + +``` From 08000fd74f03bb8048a9e9cea1350a7a065f818b Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Thu, 3 Jul 2025 09:55:50 +0530 Subject: [PATCH 12/13] Update src/main/java/io/appium/java_client/proxy/Helpers.java Co-authored-by: Valery Yatsynovich --- src/main/java/io/appium/java_client/proxy/Helpers.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/appium/java_client/proxy/Helpers.java b/src/main/java/io/appium/java_client/proxy/Helpers.java index 880c45e7e..e420d494e 100644 --- a/src/main/java/io/appium/java_client/proxy/Helpers.java +++ b/src/main/java/io/appium/java_client/proxy/Helpers.java @@ -148,7 +148,8 @@ public static T createProxy( listeners.stream() .filter(ProxyAwareListener.class::isInstance) - .forEach(listener -> ((ProxyAwareListener) listener).attachProxyInstance(result)); + .map(ProxyAwareListener.class::cast) + .forEach(listener -> listener.attachProxyInstance(result)); return result; } catch (SecurityException | ReflectiveOperationException e) { From fdbdadb901ef057978344d8680e0cd04f81b344d Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Thu, 3 Jul 2025 09:56:36 +0530 Subject: [PATCH 13/13] Optimize method --- .../proxy/ElementAwareWebDriverListener.java | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/main/java/io/appium/java_client/proxy/ElementAwareWebDriverListener.java b/src/main/java/io/appium/java_client/proxy/ElementAwareWebDriverListener.java index 76e529dee..3540b5e7d 100644 --- a/src/main/java/io/appium/java_client/proxy/ElementAwareWebDriverListener.java +++ b/src/main/java/io/appium/java_client/proxy/ElementAwareWebDriverListener.java @@ -69,16 +69,13 @@ public Object call(Object obj, Method method, Object[] args, Callable origina Object result = original.call(); if (result instanceof RemoteWebElement) { - return wrapElement( - (RemoteWebElement) result, - parent, - this); + return wrapElement((RemoteWebElement) result); } if (result instanceof List) { return ((List) result).stream() .map(item -> item instanceof RemoteWebElement ? wrapElement( - (RemoteWebElement) item, parent, this) : item) + (RemoteWebElement) item) : item) .collect(Collectors.toList()); } @@ -86,15 +83,13 @@ public Object call(Object obj, Method method, Object[] args, Callable origina } private RemoteWebElement wrapElement( - RemoteWebElement original, - WebDriver parent, - MethodCallListener listener + RemoteWebElement original ) { RemoteWebElement proxy = createProxy( RemoteWebElement.class, new Object[]{}, new Class[]{}, - Collections.singletonList(listener), + Collections.singletonList(this), ElementMatchers.not( namedOneOf( OBJECT_METHOD_NAMES.toArray(new String[0]))