Skip to content

feat: Proxy commands issues via RemoteWebElement #2311

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jul 3, 2025
Merged
5 changes: 5 additions & 0 deletions src/main/java/io/appium/java_client/proxy/Helpers.java
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ public static <T> 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* 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<MethodCallListener> listeners;

public RemoteWebElementListener(MethodCallListener listener) {
this.listeners = Collections.singletonList(listener);
}

public RemoteWebElementListener(Collection<MethodCallListener> 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) {
return wrapElement(
(RemoteWebElement) result,
parent,
listeners);
}

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;
}

private RemoteWebElement wrapElement(
RemoteWebElement original,
WebDriver parent,
Collection<MethodCallListener> 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;
}

}
81 changes: 81 additions & 0 deletions src/test/java/io/appium/java_client/proxy/ProxyHelpersTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<WebElement> findElements(By locator) {
List<WebElement> 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
Expand Down Expand Up @@ -133,4 +164,54 @@ 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");
}
};

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)
);

WebElement element = driver.findElement(By.id("button"));

assertThrows(
NoSuchSessionException.class,
element::click
);

List<WebElement> 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"
)
)));
}
}
Loading