Skip to content
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