diff --git a/java/client/src/org/openqa/selenium/devtools/DevToolsProvider.java b/java/client/src/org/openqa/selenium/devtools/DevToolsProvider.java index 2add79b14e6e1..f4a445168e517 100644 --- a/java/client/src/org/openqa/selenium/devtools/DevToolsProvider.java +++ b/java/client/src/org/openqa/selenium/devtools/DevToolsProvider.java @@ -19,16 +19,15 @@ import com.google.auto.service.AutoService; import org.openqa.selenium.Capabilities; -import org.openqa.selenium.internal.Require; import org.openqa.selenium.remote.AugmenterProvider; -import org.openqa.selenium.remote.InterfaceImplementation; +import org.openqa.selenium.remote.ExecuteMethod; import java.util.Map; import java.util.Optional; import java.util.function.Predicate; @AutoService(AugmenterProvider.class) -public class DevToolsProvider implements AugmenterProvider { +public class DevToolsProvider implements AugmenterProvider { @Override public Predicate isApplicable() { @@ -36,25 +35,15 @@ public Predicate isApplicable() { } @Override - public Class getDescribedInterface() { + public Class getDescribedInterface() { return HasDevTools.class; } @Override - public InterfaceImplementation getImplementation(Object value) { - Require.argument("Implementation", value).instanceOf(Capabilities.class); + public HasDevTools getImplementation(Capabilities caps, ExecuteMethod executeMethod) { + Optional devTools = SeleniumCdpConnection.create(caps).map(DevTools::new); - Capabilities caps = (Capabilities) value; - Optional devTools = SeleniumCdpConnection.create(caps) - .map(DevTools::new); - - return (executeMethod, self, method, args) -> { - if (!"getDevTools".equals(method.getName())) { - throw new IllegalStateException("Unexpected call to " + method); - } - - return devTools.orElseThrow(() -> new IllegalStateException("Unable to create connection to " + caps)); - }; + return () -> devTools.orElseThrow(() -> new IllegalStateException("Unable to create connection to " + caps)); } private String getCdpUrl(Capabilities caps) { diff --git a/java/client/src/org/openqa/selenium/remote/AddRotatable.java b/java/client/src/org/openqa/selenium/remote/AddRotatable.java index 3873ef147b41a..707e409cc6c58 100644 --- a/java/client/src/org/openqa/selenium/remote/AddRotatable.java +++ b/java/client/src/org/openqa/selenium/remote/AddRotatable.java @@ -17,45 +17,56 @@ package org.openqa.selenium.remote; -import com.google.common.collect.ImmutableMap; - -import org.openqa.selenium.DeviceRotation; +import org.openqa.selenium.Capabilities; import org.openqa.selenium.Rotatable; -import org.openqa.selenium.ScreenOrientation; -public class AddRotatable implements AugmenterProvider { +import java.util.function.Predicate; + +import static org.openqa.selenium.remote.CapabilityType.ROTATABLE; + +public class AddRotatable implements AugmenterProvider { @Override - public Class getDescribedInterface() { + public Predicate isApplicable() { + return caps -> caps.is(ROTATABLE); + } + + @Override + public Class getDescribedInterface() { return Rotatable.class; } @Override - public InterfaceImplementation getImplementation(Object value) { - return (executeMethod, self, method, args) -> { - String m = method.getName(); - Object response; - switch(m) { - case "rotate": - if (args[0] instanceof ScreenOrientation) { - response = executeMethod.execute(DriverCommand.SET_SCREEN_ORIENTATION, ImmutableMap.of("orientation", args[0])); - } else if (args[0] instanceof DeviceRotation) { - response = executeMethod.execute(DriverCommand.SET_SCREEN_ORIENTATION, ((DeviceRotation)args[0]).parameters()); - } else { - throw new IllegalArgumentException("rotate parameter must be either of type 'ScreenOrientation' or 'DeviceRotation'"); - } - break; - case "getOrientation": - response = ScreenOrientation.valueOf((String) executeMethod.execute(DriverCommand.GET_SCREEN_ORIENTATION, null)); - break; - case "rotation": - response = executeMethod.execute(DriverCommand.GET_SCREEN_ROTATION, null); - break; - default: - throw new IllegalArgumentException(method.getName() + ", Not defined in rotatable interface"); - } - return response; - }; + public Rotatable getImplementation(Capabilities capabilities, ExecuteMethod executeMethod) { + return new RemoteRotatable(executeMethod); } +// @Override +// public InterfaceImplementation getImplementation(Object value) { +// return (executeMethod, self, method, args) -> { +// String m = method.getName(); +// Object response; +// switch(m) { +// case "rotate": +// if (args[0] instanceof ScreenOrientation) { +// response = executeMethod.execute(DriverCommand.SET_SCREEN_ORIENTATION, ImmutableMap.of("orientation", args[0])); +// } else if (args[0] instanceof DeviceRotation) { +// response = executeMethod.execute(DriverCommand.SET_SCREEN_ORIENTATION, ((DeviceRotation)args[0]).parameters()); +// } else { +// throw new IllegalArgumentException("rotate parameter must be either of type 'ScreenOrientation' or 'DeviceRotation'"); +// } +// break; +// case "getOrientation": +// response = ScreenOrientation.valueOf((String) executeMethod.execute(DriverCommand.GET_SCREEN_ORIENTATION, null)); +// break; +// case "rotation": +// response = executeMethod.execute(DriverCommand.GET_SCREEN_ROTATION, null); +// break; +// default: +// throw new IllegalArgumentException(method.getName() + ", Not defined in rotatable interface"); +// } +// return response; +// }; +// } + } diff --git a/java/client/src/org/openqa/selenium/remote/Augmenter.java b/java/client/src/org/openqa/selenium/remote/Augmenter.java index 5b2302e2db747..facd57152ad42 100644 --- a/java/client/src/org/openqa/selenium/remote/Augmenter.java +++ b/java/client/src/org/openqa/selenium/remote/Augmenter.java @@ -17,67 +17,194 @@ package org.openqa.selenium.remote; - -import static net.bytebuddy.matcher.ElementMatchers.any; -import static net.bytebuddy.matcher.ElementMatchers.named; - -import com.google.common.collect.ImmutableList; - import net.bytebuddy.ByteBuddy; import net.bytebuddy.description.annotation.AnnotationDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; import net.bytebuddy.implementation.FixedValue; -import net.bytebuddy.implementation.InvocationHandlerAdapter; - +import net.bytebuddy.implementation.MethodDelegation; +import org.openqa.selenium.Beta; import org.openqa.selenium.Capabilities; +import org.openqa.selenium.HasCapabilities; import org.openqa.selenium.ImmutableCapabilities; import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.WrapsDriver; +import org.openqa.selenium.internal.Require; +import org.openqa.selenium.remote.html5.AddApplicationCache; +import org.openqa.selenium.remote.html5.AddLocationContext; +import org.openqa.selenium.remote.html5.AddWebStorage; +import org.openqa.selenium.remote.mobile.AddNetworkConnection; import java.lang.reflect.Field; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.HashMap; +import java.util.Collections; import java.util.HashSet; -import java.util.Map; +import java.util.List; +import java.util.ServiceLoader; import java.util.Set; +import java.util.function.BiFunction; import java.util.function.Predicate; -import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import static net.bytebuddy.matcher.ElementMatchers.anyOf; +import static net.bytebuddy.matcher.ElementMatchers.named; /** * Enhance the interfaces implemented by an instance of the - * {@link org.openqa.selenium.remote.RemoteWebDriver} based on the returned + * {@link org.openqa.selenium.WebDriver} based on the returned * {@link org.openqa.selenium.Capabilities} of the driver. * * Note: this class is still experimental. Use at your own risk. */ -public class Augmenter extends BaseAugmenter { +@Beta +public class Augmenter { + private final Set> augmentations; + + public Augmenter() { + this.augmentations = Collections.emptySet(); + addDriverAugmentation(new AddApplicationCache()); + addDriverAugmentation(new AddLocationContext()); + addDriverAugmentation(new AddNetworkConnection()); + addDriverAugmentation(new AddRotatable()); + addDriverAugmentation(new AddWebStorage()); + + StreamSupport.stream(ServiceLoader.load(AugmenterProvider.class).spliterator(), false) + .forEach(this::addDriverAugmentation); + } + + private Augmenter(Set> augmentations, Augmentation toAdd) { + Require.nonNull("Current list of augmentations", augmentations); + Require.nonNull("Augmentation to add", toAdd); - private static final Logger logger = Logger.getLogger(Augmenter.class.getName()); + Set> toUse = new HashSet<>(augmentations.size() + 1); + toUse.addAll(augmentations); + toUse.add(toAdd); + + this.augmentations = Collections.unmodifiableSet(toUse); + } + + public Augmenter addDriverAugmentation(AugmenterProvider provider) { + Require.nonNull("Interface provider", provider); + + return addDriverAugmentation( + provider.isApplicable(), + provider.getDescribedInterface(), + provider::getImplementation); + } + + public Augmenter addDriverAugmentation( + String capabilityName, + Class implementThis, + BiFunction usingThis) { + Require.nonNull("Capability name", capabilityName); + Require.nonNull("Interface to implement", implementThis); + Require.nonNull("Concrete implementation", usingThis, "of %s", implementThis); + + return addDriverAugmentation(check(capabilityName), implementThis, usingThis); + } - @Override - protected X create( - RemoteWebDriver driver, - Map, AugmenterProvider> augmentors, - X objectToAugment) { - CompoundHandler handler = determineAugmentation(driver, augmentors, objectToAugment); + public Augmenter addDriverAugmentation( + Predicate whenThisMatches, + Class implementThis, + BiFunction usingThis) { + Require.nonNull("Capability predicate", whenThisMatches); + Require.nonNull("Interface to implement", implementThis); + Require.nonNull("Concrete implementation", usingThis, "of %s", implementThis); + Require.precondition(implementThis.isInterface(), "Expected %s to be an interface", implementThis); - X augmented = performAugmentation(handler, objectToAugment); + return new Augmenter(augmentations, new Augmentation<>(whenThisMatches, implementThis, usingThis)); + } - copyFields(objectToAugment.getClass(), objectToAugment, augmented); + private Predicate check(String capabilityName) { + return caps -> { + Require.nonNull("Capability name", capabilityName); - return augmented; + Object value = caps.getCapability(capabilityName); + if (value instanceof Boolean && !((Boolean) value)) { + return false; + } + return value != null; + }; } - @Override - protected RemoteWebDriver extractRemoteWebDriver(WebDriver driver) { - if (driver.getClass().isAnnotationPresent(Augmentable.class)) { + /** + * Enhance the interfaces implemented by this instance of WebDriver iff that instance is a + * {@link org.openqa.selenium.remote.RemoteWebDriver}. + * + * The WebDriver that is returned may well be a dynamic proxy. You cannot rely on the concrete + * implementing class to remain constant. + * + * @param driver The driver to enhance + * @return A class implementing the described interfaces. + */ + public WebDriver augment(WebDriver driver) { + Require.nonNull("WebDriver", driver); + Require.precondition(driver instanceof HasCapabilities, "Driver must have capabilities", driver); + + Capabilities caps = ImmutableCapabilities.copyOf(((HasCapabilities) driver).getCapabilities()); + + // Collect the interfaces to apply + List> matchingAugmenters = augmentations.stream() + // Only consider the augmenters that match interfaces we don't already implement + .filter(augmentation -> !augmentation.interfaceClass.isAssignableFrom(driver.getClass())) + // And which match an augmentation we have + .filter(augmentation -> augmentation.whenMatches.test(caps)) + .collect(Collectors.toList()); + + if (matchingAugmenters.isEmpty()) { + return driver; + } + + // Grab a remote execution method, if possible + RemoteWebDriver remote = extractRemoteWebDriver(driver); + ExecuteMethod execute = remote == null ? + (commandName, parameters) -> { throw new WebDriverException("Cannot execute remote command: " + commandName); } : + new RemoteExecuteMethod(remote); + + DynamicType.Builder builder = new ByteBuddy() + .subclass(driver.getClass()) + .annotateType(AnnotationDescription.Builder.ofType(Augmentable.class).build()) + .method(named("isAugmented")).intercept(FixedValue.value(true)); + + for (Augmentation augmentation : augmentations) { + Class iface = augmentation.interfaceClass; + + Object instance = augmentation.implementation.apply(caps, execute); + + builder = builder.implement(iface) + .method(anyOf(iface.getDeclaredMethods())) + .intercept(MethodDelegation.to(instance, iface)); + } + + Class definition = builder.make() + .load(driver.getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) + .getLoaded() + .asSubclass(driver.getClass()); + + try { + WebDriver toReturn = definition.getDeclaredConstructor().newInstance(); + + copyFields(driver.getClass(), driver, toReturn); + + return toReturn; + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Unable to create new proxy", e); + } + } + + private RemoteWebDriver extractRemoteWebDriver(WebDriver driver) { + Require.nonNull("WebDriver", driver); + + if (driver instanceof RemoteWebDriver) { return (RemoteWebDriver) driver; } - logger.warning("Augmenter should be applied to the instances of @Augmentable classes " + - "or previously augmented instances only (instance class was: " + driver.getClass() + ")"); + if (driver instanceof WrapsDriver) { + return extractRemoteWebDriver(((WrapsDriver) driver).getWrappedDriver()); + } + return null; } @@ -108,93 +235,23 @@ private void copyField(Object source, Object target, Field field) { } } - private CompoundHandler determineAugmentation( - RemoteWebDriver driver, - Map, AugmenterProvider> augmentors, - Object objectToAugment) { - CompoundHandler handler = new CompoundHandler(driver, objectToAugment); - - Capabilities capabilities = ImmutableCapabilities.copyOf(driver.getCapabilities()); - - for (Map.Entry, AugmenterProvider> entry : augmentors.entrySet()) { - if (!entry.getKey().test(capabilities)) { - continue; - } - - AugmenterProvider augmenter = entry.getValue(); - handler.addCapabilityHander(augmenter.getDescribedInterface(), augmenter.getImplementation(capabilities)); - } - return handler; - } - - @SuppressWarnings({"unchecked"}) - protected X performAugmentation(CompoundHandler handler, X from) { - if (handler.isNeedingApplication()) { - Class superClass = from.getClass(); - - Class loaded = new ByteBuddy() - .subclass(superClass) - .implement(ImmutableList.copyOf(handler.getInterfaces())) - .annotateType(AnnotationDescription.Builder.ofType(Augmentable.class).build()) - .method(any()).intercept(InvocationHandlerAdapter.of(handler)) - .method(named("isAugmented")).intercept(FixedValue.value(true)) - .make() - .load(superClass.getClassLoader()) - .getLoaded() - .asSubclass(from.getClass()); - - try { - return (X) loaded.getDeclaredConstructor().newInstance(); - } catch (ReflectiveOperationException e) { - throw new RuntimeException("Unable to create subclass", e); - } - } - - return from; - } - - private class CompoundHandler implements InvocationHandler { - - private final ExecuteMethod execute; - private final Object originalInstance; - private final Map handlers = new HashMap<>(); - private final Set> interfaces = new HashSet<>(); - - private CompoundHandler(RemoteWebDriver driver, Object originalInstance) { - this.execute = new RemoteExecuteMethod(driver); - this.originalInstance = originalInstance; - } - - void addCapabilityHander(Class fromInterface, InterfaceImplementation handledBy) { - if (fromInterface.isInterface()) { - interfaces.add(fromInterface); - } - for (Method method : fromInterface.getDeclaredMethods()) { - handlers.put(method, handledBy); - } - } - - Set> getInterfaces() { - return interfaces; - } - - boolean isNeedingApplication() { - return !handlers.isEmpty(); - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - InterfaceImplementation handler = handlers.get(method); - - if (handler == null) { - try { - return method.invoke(originalInstance, args); - } catch (InvocationTargetException e) { - throw e.getTargetException(); - } - } - - return handler.invoke(execute, proxy, method, args); + private static class Augmentation { + public final Predicate whenMatches; + public final Class interfaceClass; + public final BiFunction implementation; + + public Augmentation( + Predicate whenMatches, + Class interfaceClass, + BiFunction implementation) { + this.whenMatches = Require.nonNull("Capabilities predicate", whenMatches); + this.interfaceClass = Require.nonNull("Interface to implement", interfaceClass); + this.implementation = Require.nonNull("Interface implementation", implementation); + + Require.precondition( + interfaceClass.isInterface(), + "%s must be an interface, not a concrete class", + interfaceClass); } } } diff --git a/java/client/src/org/openqa/selenium/remote/AugmenterProvider.java b/java/client/src/org/openqa/selenium/remote/AugmenterProvider.java index 809f30e515ef9..6bb1589964b1e 100644 --- a/java/client/src/org/openqa/selenium/remote/AugmenterProvider.java +++ b/java/client/src/org/openqa/selenium/remote/AugmenterProvider.java @@ -25,25 +25,22 @@ * Describes and provides an implementation for a particular interface for use with the * {@link org.openqa.selenium.remote.Augmenter}. Think of this as a simulacrum of mixins. */ -public interface AugmenterProvider { +public interface AugmenterProvider { /** * @return Whether this provider should be applied given these {@code caps}. */ - default Predicate isApplicable() { - return caps -> false; - } + Predicate isApplicable(); /** * @return The interface that this augmentor describes. */ - Class getDescribedInterface(); + Class getDescribedInterface(); /** * For the interface that this provider describes, return an implementation. * - * @param value The value from the capability map * @return An interface implementation */ - InterfaceImplementation getImplementation(Object value); + X getImplementation(Capabilities capabilities, ExecuteMethod executeMethod); } diff --git a/java/client/src/org/openqa/selenium/remote/BUILD.bazel b/java/client/src/org/openqa/selenium/remote/BUILD.bazel index d059fef6f25b3..d75d0b3e7efe1 100644 --- a/java/client/src/org/openqa/selenium/remote/BUILD.bazel +++ b/java/client/src/org/openqa/selenium/remote/BUILD.bazel @@ -51,9 +51,7 @@ java_export( "AddRotatable.java", "Augmenter.java", "AugmenterProvider.java", - "BaseAugmenter.java", - "InterfaceImplementation.java", - "JdkAugmenter.java", + "RemoteRotatable.java", "RemoteTags.java", "html5/AddApplicationCache.java", "html5/AddLocationContext.java", diff --git a/java/client/src/org/openqa/selenium/remote/BaseAugmenter.java b/java/client/src/org/openqa/selenium/remote/BaseAugmenter.java deleted file mode 100644 index a59a123d66a68..0000000000000 --- a/java/client/src/org/openqa/selenium/remote/BaseAugmenter.java +++ /dev/null @@ -1,172 +0,0 @@ -// Licensed to the Software Freedom Conservancy (SFC) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The SFC licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. 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 org.openqa.selenium.remote; - -import org.openqa.selenium.Capabilities; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.internal.Require; -import org.openqa.selenium.remote.html5.AddApplicationCache; -import org.openqa.selenium.remote.html5.AddLocationContext; -import org.openqa.selenium.remote.html5.AddWebStorage; -import org.openqa.selenium.remote.mobile.AddNetworkConnection; - -import java.util.HashMap; -import java.util.Map; -import java.util.ServiceLoader; -import java.util.function.Predicate; -import java.util.stream.StreamSupport; - -import static org.openqa.selenium.remote.CapabilityType.ROTATABLE; -import static org.openqa.selenium.remote.CapabilityType.SUPPORTS_APPLICATION_CACHE; -import static org.openqa.selenium.remote.CapabilityType.SUPPORTS_LOCATION_CONTEXT; -import static org.openqa.selenium.remote.CapabilityType.SUPPORTS_NETWORK_CONNECTION; -import static org.openqa.selenium.remote.CapabilityType.SUPPORTS_WEB_STORAGE; - -/** - * Enhance the interfaces implemented by an instance of the - * {@link org.openqa.selenium.remote.RemoteWebDriver} based on the returned - * {@link org.openqa.selenium.Capabilities} of the driver. - * - * Note: this class is still experimental. Use at your own risk. - */ -public abstract class BaseAugmenter { - private final Map, AugmenterProvider> driverAugmentors = new HashMap<>(); - private final Map, AugmenterProvider> elementAugmentors = new HashMap<>(); - - public BaseAugmenter() { - addDriverAugmentation(SUPPORTS_LOCATION_CONTEXT, new AddLocationContext()); - addDriverAugmentation(SUPPORTS_APPLICATION_CACHE, new AddApplicationCache()); - addDriverAugmentation(SUPPORTS_NETWORK_CONNECTION, new AddNetworkConnection()); - addDriverAugmentation(SUPPORTS_WEB_STORAGE, new AddWebStorage()); - addDriverAugmentation(ROTATABLE, new AddRotatable()); - - StreamSupport.stream(ServiceLoader.load(AugmenterProvider.class).spliterator(), false) - .forEach(provider -> { - driverAugmentors.put(provider.isApplicable(), provider); - }); - } - - /** - * Add a mapping between a capability name and the implementation of the interface that name - * represents for instances of {@link org.openqa.selenium.WebDriver}. - *

- * Note: This method is still experimental. Use at your own risk. - * - * @param capabilityName The name of the capability to model - * @param handlerClass The provider of the interface and implementation - */ - public void addDriverAugmentation(String capabilityName, AugmenterProvider handlerClass) { - driverAugmentors.put(check(capabilityName), handlerClass); - } - - public void addDriverAugmentation(Predicate predicate, AugmenterProvider handlerClass) { - Require.nonNull("Predicate", predicate); - Require.nonNull("Handler class", handlerClass); - driverAugmentors.put(predicate, handlerClass); - } - - /** - * Add a mapping between a capability name and the implementation of the interface that name - * represents for instances of {@link org.openqa.selenium.WebElement}. - *

- * Note: This method is still experimental. Use at your own risk. - * - * @param capabilityName The name of the capability to model - * @param handlerClass The provider of the interface and implementation - */ - public void addElementAugmentation(String capabilityName, AugmenterProvider handlerClass) { - elementAugmentors.put(check(capabilityName), handlerClass); - } - - public void addElementAugmentation(Predicate predicate, AugmenterProvider handlerClass) { - Require.nonNull("Predicate", predicate); - Require.nonNull("Handler class", handlerClass); - elementAugmentors.put(predicate, handlerClass); - } - - private Predicate check(String capabilityName) { - return caps -> { - Require.nonNull("Capability name", capabilityName); - - Object value = caps.getCapability(capabilityName); - if (value instanceof Boolean && !((Boolean) value)) { - return false; - } - return value != null; - }; - } - - /** - * Enhance the interfaces implemented by this instance of WebDriver iff that instance is a - * {@link org.openqa.selenium.remote.RemoteWebDriver}. - * - * The WebDriver that is returned may well be a dynamic proxy. You cannot rely on the concrete - * implementing class to remain constant. - * - * @param driver The driver to enhance - * @return A class implementing the described interfaces. - */ - public WebDriver augment(WebDriver driver) { - RemoteWebDriver remoteDriver = extractRemoteWebDriver(driver); - if (remoteDriver == null) { - return driver; - } - return create(remoteDriver, driverAugmentors, driver); - } - - /** - * Enhance the interfaces implemented by this instance of WebElement iff that instance is a - * {@link org.openqa.selenium.remote.RemoteWebElement}. - * - * The WebElement that is returned may well be a dynamic proxy. You cannot rely on the concrete - * implementing class to remain constant. - * - * @param element The driver to enhance. - * @return A class implementing the described interfaces. - */ - public WebElement augment(RemoteWebElement element) { - // TODO(simon): We should really add a "SelfDescribing" interface for this - RemoteWebDriver parent = (RemoteWebDriver) element.getWrappedDriver(); - if (parent == null) { - return element; - } - - return create(parent, elementAugmentors, element); - } - - /** - * Subclasses should perform the requested augmentation. - * - * @param typically a RemoteWebDriver or RemoteWebElement - * @param augmentors augumentors to augment the object - * @param driver RWD instance - * @param objectToAugment object to augment - * @return an augmented version of objectToAugment. - */ - protected abstract X create(RemoteWebDriver driver, Map, AugmenterProvider> augmentors, - X objectToAugment); - - /** - * Subclasses should extract the remote webdriver or return null if it can't extract it. - * - * @param driver WebDriver instance to extract - * @return extracted RemoteWebDriver or null - */ - protected abstract RemoteWebDriver extractRemoteWebDriver(WebDriver driver); -} diff --git a/java/client/src/org/openqa/selenium/remote/InterfaceImplementation.java b/java/client/src/org/openqa/selenium/remote/InterfaceImplementation.java deleted file mode 100644 index 62eb1f21dc084..0000000000000 --- a/java/client/src/org/openqa/selenium/remote/InterfaceImplementation.java +++ /dev/null @@ -1,40 +0,0 @@ -// Licensed to the Software Freedom Conservancy (SFC) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The SFC licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. 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 org.openqa.selenium.remote; - -import java.lang.reflect.Method; - -/** - * An implementation of a particular interface, used by the - * {@link org.openqa.selenium.remote.Augmenter}. - */ -public interface InterfaceImplementation { - - /** - * Called when it has become apparent that this is the right interface to implement a particular - * method. - * - * @param executeMethod Call this to actually call the remote instance - * @param self aka this - * @param method The method invoked by the user - * @param args The arguments to the method @return The return value, which will be passed - * to the user directly. - * @return object returned from the method invoked - */ - Object invoke(ExecuteMethod executeMethod, Object self, Method method, Object... args); -} diff --git a/java/client/src/org/openqa/selenium/remote/JdkAugmenter.java b/java/client/src/org/openqa/selenium/remote/JdkAugmenter.java deleted file mode 100644 index a6ae77626c113..0000000000000 --- a/java/client/src/org/openqa/selenium/remote/JdkAugmenter.java +++ /dev/null @@ -1,141 +0,0 @@ -// Licensed to the Software Freedom Conservancy (SFC) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The SFC licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. 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 org.openqa.selenium.remote; - -import com.google.common.reflect.AbstractInvocationHandler; -import org.openqa.selenium.Beta; -import org.openqa.selenium.Capabilities; -import org.openqa.selenium.ImmutableCapabilities; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.internal.Require; - -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.function.Predicate; - -/** - * Enhance the interfaces implemented by an instance of the - * {@link org.openqa.selenium.remote.RemoteWebDriver} based on the returned - * {@link org.openqa.selenium.Capabilities} of the driver. - * - * Note: this class is still experimental. Use at your own risk. - */ -@Beta -public class JdkAugmenter extends BaseAugmenter { - - public JdkAugmenter() { - super(); - } - - @Override - protected RemoteWebDriver extractRemoteWebDriver(WebDriver driver) { - if (driver instanceof RemoteWebDriver) { - return (RemoteWebDriver) driver; - } else if (Proxy.isProxyClass(driver.getClass())) { - InvocationHandler handler = Proxy.getInvocationHandler(driver); - if (handler instanceof JdkHandler) { - return ((JdkHandler) handler).driver; - } - } - return null; - } - - @Override - protected X create( - RemoteWebDriver driver, - Map, AugmenterProvider> augmentors, - X objectToAugment) { - Capabilities capabilities = ImmutableCapabilities.copyOf(driver.getCapabilities()); - Map augmentationHandlers = new HashMap<>(); - - Set> proxiedInterfaces = new HashSet<>(); - Class superClass = objectToAugment.getClass(); - - while (null != superClass) { - proxiedInterfaces.addAll(Arrays.asList(superClass.getInterfaces())); - superClass = superClass.getSuperclass(); - } - - for (Map.Entry, AugmenterProvider> entry : augmentors.entrySet()) { - if (!entry.getKey().test(capabilities)) { - continue; - } - - AugmenterProvider augmenter = entry.getValue(); - - Class interfaceProvided = augmenter.getDescribedInterface(); - Require.stateCondition(interfaceProvided.isInterface(), - "JdkAugmenter can only augment interfaces. %s is not an interface.", interfaceProvided); - proxiedInterfaces.add(interfaceProvided); - InterfaceImplementation augmentedImplementation = augmenter.getImplementation(capabilities); - for (Method method : interfaceProvided.getMethods()) { - InterfaceImplementation oldHandler = augmentationHandlers.put(method, - augmentedImplementation); - Require.stateCondition(oldHandler == null, "Both %s and %s attempt to define %s.", - oldHandler, augmentedImplementation.getClass(), method.getName()); - } - } - - if (augmentationHandlers.isEmpty()) { - // If there are no handlers, don't bother proxy'ing. - return objectToAugment; - } - - InvocationHandler proxyHandler = new JdkHandler<>(driver, - objectToAugment, augmentationHandlers); - return (X) Proxy.newProxyInstance( - getClass().getClassLoader(), - proxiedInterfaces.toArray(new Class[proxiedInterfaces.size()]), - proxyHandler); - } - - private static class JdkHandler extends AbstractInvocationHandler - implements InvocationHandler { - private final RemoteWebDriver driver; - private final X realInstance; - private final Map handlers; - - private JdkHandler(RemoteWebDriver driver, X realInstance, - Map handlers) { - super(); - this.driver = Require.nonNull("Driver", driver); - this.realInstance = Require.nonNull("Real instance", realInstance); - this.handlers = Require.nonNull("Handlers", handlers); - } - - @Override - public Object handleInvocation(Object proxy, Method method, Object[] args) throws Throwable { - InterfaceImplementation handler = handlers.get(method); - try { - if (null == handler) { - return method.invoke(realInstance, args); - } - return handler.invoke(new RemoteExecuteMethod(driver), proxy, method, args); - } catch (InvocationTargetException i) { - throw i.getCause(); - } - } - } -} diff --git a/java/client/src/org/openqa/selenium/remote/RemoteExecuteMethod.java b/java/client/src/org/openqa/selenium/remote/RemoteExecuteMethod.java index d8890005b0121..1676151fd52d8 100644 --- a/java/client/src/org/openqa/selenium/remote/RemoteExecuteMethod.java +++ b/java/client/src/org/openqa/selenium/remote/RemoteExecuteMethod.java @@ -17,13 +17,15 @@ package org.openqa.selenium.remote; +import org.openqa.selenium.internal.Require; + import java.util.Map; public class RemoteExecuteMethod implements ExecuteMethod { private final RemoteWebDriver driver; public RemoteExecuteMethod(RemoteWebDriver driver) { - this.driver = driver; + this.driver = Require.nonNull("Remote WebDriver", driver); } @Override diff --git a/java/client/src/org/openqa/selenium/remote/RemoteRotatable.java b/java/client/src/org/openqa/selenium/remote/RemoteRotatable.java new file mode 100644 index 0000000000000..ca69dab6c6fa3 --- /dev/null +++ b/java/client/src/org/openqa/selenium/remote/RemoteRotatable.java @@ -0,0 +1,62 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. 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 org.openqa.selenium.remote; + +import com.google.common.collect.ImmutableMap; +import org.openqa.selenium.DeviceRotation; +import org.openqa.selenium.Rotatable; +import org.openqa.selenium.ScreenOrientation; +import org.openqa.selenium.internal.Require; + +import java.util.Map; + +class RemoteRotatable implements Rotatable { + + private final ExecuteMethod executeMethod; + + public RemoteRotatable(ExecuteMethod executeMethod) { + this.executeMethod = Require.nonNull("Execute method", executeMethod); + } + + @Override + public void rotate(ScreenOrientation orientation) { + executeMethod.execute(DriverCommand.SET_SCREEN_ORIENTATION, ImmutableMap.of("orientation", orientation)); + } + + @Override + public ScreenOrientation getOrientation() { + return ScreenOrientation.valueOf( + (String) executeMethod.execute(DriverCommand.GET_SCREEN_ORIENTATION, null)); + } + + @Override + public void rotate(DeviceRotation rotation) { + executeMethod.execute(DriverCommand.SET_SCREEN_ORIENTATION, rotation.parameters()); + } + + @Override + public DeviceRotation rotation() { + Object result = executeMethod.execute(DriverCommand.GET_SCREEN_ROTATION, null); + if (!(result instanceof Map)) { + throw new IllegalStateException("Unexpected return value: " + result); + } + + @SuppressWarnings("unchecked") Map raw = (Map) result; + return new DeviceRotation(raw); + } +} diff --git a/java/client/src/org/openqa/selenium/remote/html5/AddApplicationCache.java b/java/client/src/org/openqa/selenium/remote/html5/AddApplicationCache.java index 81b7899cc2a85..073398458c0fe 100644 --- a/java/client/src/org/openqa/selenium/remote/html5/AddApplicationCache.java +++ b/java/client/src/org/openqa/selenium/remote/html5/AddApplicationCache.java @@ -17,33 +17,29 @@ package org.openqa.selenium.remote.html5; -import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.Capabilities; import org.openqa.selenium.html5.ApplicationCache; import org.openqa.selenium.remote.AugmenterProvider; -import org.openqa.selenium.remote.InterfaceImplementation; +import org.openqa.selenium.remote.ExecuteMethod; -import java.lang.reflect.InvocationTargetException; +import java.util.function.Predicate; -public class AddApplicationCache implements AugmenterProvider { +import static org.openqa.selenium.remote.CapabilityType.SUPPORTS_APPLICATION_CACHE; + +public class AddApplicationCache implements AugmenterProvider { @Override - public Class getDescribedInterface() { - return ApplicationCache.class; + public Predicate isApplicable() { + return caps -> caps.is(SUPPORTS_APPLICATION_CACHE); } @Override - public InterfaceImplementation getImplementation(Object value) { - return (executeMethod, self, method, args) -> { - RemoteApplicationCache cache = new RemoteApplicationCache(executeMethod); - try { - return method.invoke(cache, args); - } catch (IllegalAccessException e) { - throw new WebDriverException(e); - } catch (InvocationTargetException e) { - throw new RuntimeException(e.getCause()); - } - }; + public Class getDescribedInterface() { + return ApplicationCache.class; } - + @Override + public ApplicationCache getImplementation(Capabilities capabilities, ExecuteMethod executeMethod) { + return new RemoteApplicationCache(executeMethod); + } } diff --git a/java/client/src/org/openqa/selenium/remote/html5/AddLocationContext.java b/java/client/src/org/openqa/selenium/remote/html5/AddLocationContext.java index 02de9b57faea0..a0cf47b3e1b09 100644 --- a/java/client/src/org/openqa/selenium/remote/html5/AddLocationContext.java +++ b/java/client/src/org/openqa/selenium/remote/html5/AddLocationContext.java @@ -17,32 +17,29 @@ package org.openqa.selenium.remote.html5; -import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.Capabilities; import org.openqa.selenium.html5.LocationContext; import org.openqa.selenium.remote.AugmenterProvider; -import org.openqa.selenium.remote.InterfaceImplementation; +import org.openqa.selenium.remote.ExecuteMethod; -import java.lang.reflect.InvocationTargetException; +import java.util.function.Predicate; -public class AddLocationContext implements AugmenterProvider { +import static org.openqa.selenium.remote.CapabilityType.SUPPORTS_LOCATION_CONTEXT; + +public class AddLocationContext implements AugmenterProvider { @Override - public Class getDescribedInterface() { - return LocationContext.class; + public Predicate isApplicable() { + return caps -> caps.is(SUPPORTS_LOCATION_CONTEXT); } @Override - public InterfaceImplementation getImplementation(Object value) { - return (executeMethod, self, method, args) -> { - LocationContext context = new RemoteLocationContext(executeMethod); - try { - return method.invoke(context, args); - } catch (IllegalAccessException e) { - throw new WebDriverException(e); - } catch (InvocationTargetException e) { - throw new RuntimeException(e.getCause()); - } - }; + public Class getDescribedInterface() { + return LocationContext.class; } + @Override + public LocationContext getImplementation(Capabilities capabilities, ExecuteMethod executeMethod) { + return new RemoteLocationContext(executeMethod); + } } diff --git a/java/client/src/org/openqa/selenium/remote/html5/AddWebStorage.java b/java/client/src/org/openqa/selenium/remote/html5/AddWebStorage.java index aca6f3362dde6..5fcf5df0fa2c5 100644 --- a/java/client/src/org/openqa/selenium/remote/html5/AddWebStorage.java +++ b/java/client/src/org/openqa/selenium/remote/html5/AddWebStorage.java @@ -17,31 +17,29 @@ package org.openqa.selenium.remote.html5; -import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.Capabilities; import org.openqa.selenium.html5.WebStorage; import org.openqa.selenium.remote.AugmenterProvider; -import org.openqa.selenium.remote.InterfaceImplementation; +import org.openqa.selenium.remote.ExecuteMethod; -import java.lang.reflect.InvocationTargetException; +import java.util.function.Predicate; -public class AddWebStorage implements AugmenterProvider { +import static org.openqa.selenium.remote.CapabilityType.SUPPORTS_WEB_STORAGE; + +public class AddWebStorage implements AugmenterProvider { + + @Override + public Predicate isApplicable() { + return caps -> caps.is(SUPPORTS_WEB_STORAGE); + } @Override - public Class getDescribedInterface() { + public Class getDescribedInterface() { return WebStorage.class; } @Override - public InterfaceImplementation getImplementation(Object value) { - return (executeMethod, self, method, args) -> { - RemoteWebStorage storage = new RemoteWebStorage(executeMethod); - try { - return method.invoke(storage, args); - } catch (IllegalAccessException e) { - throw new WebDriverException(e); - } catch (InvocationTargetException e) { - throw new RuntimeException(e.getCause()); - } - }; + public WebStorage getImplementation(Capabilities capabilities, ExecuteMethod executeMethod) { + return new RemoteWebStorage(executeMethod); } } diff --git a/java/client/src/org/openqa/selenium/remote/mobile/AddNetworkConnection.java b/java/client/src/org/openqa/selenium/remote/mobile/AddNetworkConnection.java index cd054f78f10b4..4801439de95ff 100644 --- a/java/client/src/org/openqa/selenium/remote/mobile/AddNetworkConnection.java +++ b/java/client/src/org/openqa/selenium/remote/mobile/AddNetworkConnection.java @@ -17,31 +17,29 @@ package org.openqa.selenium.remote.mobile; -import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.Capabilities; import org.openqa.selenium.mobile.NetworkConnection; import org.openqa.selenium.remote.AugmenterProvider; -import org.openqa.selenium.remote.InterfaceImplementation; +import org.openqa.selenium.remote.ExecuteMethod; -import java.lang.reflect.InvocationTargetException; +import java.util.function.Predicate; -public class AddNetworkConnection implements AugmenterProvider { +import static org.openqa.selenium.remote.CapabilityType.SUPPORTS_NETWORK_CONNECTION; + +public class AddNetworkConnection implements AugmenterProvider { + + @Override + public Predicate isApplicable() { + return caps -> caps.is(SUPPORTS_NETWORK_CONNECTION); + } @Override - public Class getDescribedInterface() { + public Class getDescribedInterface() { return NetworkConnection.class; } @Override - public InterfaceImplementation getImplementation(Object value) { - return (executeMethod, self, method, args) -> { - NetworkConnection connection = new RemoteNetworkConnection(executeMethod); - try { - return method.invoke(connection, args); - } catch (IllegalAccessException e) { - throw new WebDriverException(e); - } catch (InvocationTargetException e) { - throw new RuntimeException(e.getCause()); - } - }; + public NetworkConnection getImplementation(Capabilities capabilities, ExecuteMethod executeMethod) { + return new RemoteNetworkConnection(executeMethod); } } diff --git a/java/client/test/org/openqa/selenium/remote/AugmenterTest.java b/java/client/test/org/openqa/selenium/remote/AugmenterTest.java index d35f95b49c128..070449e81d6ea 100644 --- a/java/client/test/org/openqa/selenium/remote/AugmenterTest.java +++ b/java/client/test/org/openqa/selenium/remote/AugmenterTest.java @@ -21,119 +21,302 @@ import org.junit.Test; import org.openqa.selenium.By; import org.openqa.selenium.Capabilities; +import org.openqa.selenium.HasCapabilities; import org.openqa.selenium.ImmutableCapabilities; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.Rotatable; +import org.openqa.selenium.ScreenOrientation; import org.openqa.selenium.SearchContext; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; +import org.openqa.selenium.firefox.FirefoxOptions; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.Mockito.mock; -import static org.openqa.selenium.remote.CapabilityType.SUPPORTS_JAVASCRIPT; import static org.openqa.selenium.remote.DriverCommand.FIND_ELEMENT; -public class AugmenterTest extends BaseAugmenterTest { +public class AugmenterTest { - @Override - public BaseAugmenter getAugmenter() { + protected Augmenter getAugmenter() { return new Augmenter(); } @Test - public void canUseTheAugmenterToInterceptConcreteMethodCalls() throws Exception { - Capabilities caps = new ImmutableCapabilities(SUPPORTS_JAVASCRIPT, true); - StubExecutor stubExecutor = new StubExecutor(caps); - stubExecutor.expect(DriverCommand.GET_TITLE, new HashMap<>(), "StubTitle"); - - final WebDriver driver = new RemoteWebDriver(stubExecutor, caps); + public void shouldAddInterfaceFromCapabilityIfNecessary() { + final Capabilities caps = new ImmutableCapabilities("magic.numbers", true); + WebDriver driver = new RemoteWebDriver(new StubExecutor(caps), caps); - // Our AugmenterProvider needs to target the class that declares quit(), - // otherwise the Augmenter won't apply the method interceptor. - final Method quitMethod = driver.getClass().getMethod("quit"); + WebDriver returned = getAugmenter() + .addDriverAugmentation("magic.numbers", HasMagicNumbers.class, (c, exe) -> () -> 42) + .augment(driver); - AugmenterProvider augmentation = new AugmenterProvider() { - @Override - public Class getDescribedInterface() { - return quitMethod.getDeclaringClass(); - } + assertThat(returned).isNotSameAs(driver); + assertThat(returned).isInstanceOf(HasMagicNumbers.class); + } - @Override - public InterfaceImplementation getImplementation(Object value) { - return (executeMethod, self, method, args) -> { - if (quitMethod.equals(method)) { - return null; - } - - try { - return method.invoke(driver, args); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } catch (InvocationTargetException e) { - throw new RuntimeException(e.getTargetException()); - } - }; - } - }; + @Test + public void shouldNotAddInterfaceWhenBooleanValueForItIsFalse() { + Capabilities caps = new ImmutableCapabilities("magic.numbers", false); + WebDriver driver = new RemoteWebDriver(new StubExecutor(caps), caps); - BaseAugmenter augmenter = getAugmenter(); + WebDriver returned = getAugmenter() + .addDriverAugmentation("magic.numbers", HasMagicNumbers.class, (c, exe) -> () -> 42) + .augment(driver); - // Set the capability that triggers the augmentation. - augmenter.addDriverAugmentation(CapabilityType.SUPPORTS_JAVASCRIPT, augmentation); + assertThat(returned).isSameAs(driver); + assertThat(returned).isNotInstanceOf(HasMagicNumbers.class); + } - WebDriver returned = augmenter.augment(driver); - assertThat(returned).isNotSameAs(driver); - assertThat(returned.getTitle()).isEqualTo("StubTitle"); + @Test + public void shouldDelegateToHandlerIfAdded() { + Capabilities caps = new ImmutableCapabilities("foo", true); + WebDriver driver = new RemoteWebDriver(new StubExecutor(caps), caps); - returned.quit(); // Should not fail because it's intercepted. + WebDriver returned = getAugmenter() + .addDriverAugmentation( + "foo", + MyInterface.class, + (c, exe) -> () -> "Hello World") + .augment(driver); - // Verify original is unmodified. - assertThatExceptionOfType(AssertionError.class) - .isThrownBy(driver::quit) - .withMessageStartingWith("Unexpected method invocation"); + String text = ((MyInterface) returned).getHelloWorld(); + assertThat(text).isEqualTo("Hello World"); } @Test - public void shouldNotAugmentRemoteWebDriverWithoutExtraCapabilities() { - Capabilities caps = new ImmutableCapabilities(); + public void shouldDelegateUnmatchedMethodCallsToDriverImplementation() { + Capabilities caps = new ImmutableCapabilities("magic.numbers", true); StubExecutor stubExecutor = new StubExecutor(caps); + stubExecutor.expect(DriverCommand.GET_TITLE, new HashMap<>(), "Title"); WebDriver driver = new RemoteWebDriver(stubExecutor, caps); - WebDriver augmentedDriver = getAugmenter().augment(driver); + WebDriver returned = getAugmenter() + .addDriverAugmentation( + "magic.numbers", + HasMagicNumbers.class, + (c, exe) -> () -> 42) + .augment(driver); - assertThat(augmentedDriver).isSameAs(driver); + assertThat(returned.getTitle()).isEqualTo("Title"); } @Test - public void shouldAugmentRemoteWebDriverWithExtraCapabilities() { - Capabilities caps = new ImmutableCapabilities(CapabilityType.SUPPORTS_WEB_STORAGE, true); - StubExecutor stubExecutor = new StubExecutor(caps); - WebDriver driver = new RemoteWebDriver(stubExecutor, caps); + public void proxyShouldNotAppearInStackTraces() { + // This will force the class to be enhanced + final Capabilities caps = new ImmutableCapabilities("magic.numbers", true); + + DetonatingDriver driver = new DetonatingDriver(); + driver.setCapabilities(caps); + + WebDriver returned = getAugmenter() + .addDriverAugmentation( + "magic.numbers", + HasMagicNumbers.class, + (c, exe) -> () -> 42) + .augment(driver); + + assertThatExceptionOfType(NoSuchElementException.class) + .isThrownBy(() -> returned.findElement(By.id("ignored"))); + } + + @Test + public void shouldCopyFieldsFromTemplateInstanceIntoChildInstance() { + ChildRemoteDriver driver = new ChildRemoteDriver(); + HasMagicNumbers holder = (HasMagicNumbers) getAugmenter().augment(driver); + + assertThat(holder.getMagicNumber()).isEqualTo(3); + } + + @Test + public void shouldNotChokeOnFinalFields() { + WithFinals withFinals = new WithFinals(); + getAugmenter().augment(withFinals); + } + + @Test + public void shouldAllowReflexiveCalls() { + Capabilities caps = new ImmutableCapabilities("find by magic", true); + StubExecutor executor = new StubExecutor(caps); + final WebElement element = mock(WebElement.class); + executor.expect( + FIND_ELEMENT, + ImmutableMap.of("using", "magic", "value", "cheese"), + element); - WebDriver augmentedDriver = getAugmenter().augment(driver); + WebDriver driver = new RemoteWebDriver(executor, caps); + WebDriver returned = getAugmenter() + .addDriverAugmentation( + "find by magic", + FindByMagic.class, + (c, exe) -> magicWord -> element) + .augment(driver); - assertThat(augmentedDriver).isNotSameAs(driver); + // No exception is a Good Thing + WebElement seen = returned.findElement(new ByMagic("cheese")); + assertThat(seen).isSameAs(element); } - public static class RemoteWebDriverSubclass extends RemoteWebDriver { - public RemoteWebDriverSubclass(CommandExecutor stubExecutor, Capabilities caps) { - super(stubExecutor, caps); + private static class ByMagic extends By { + private final String magicWord; + + public ByMagic(String magicWord) { + this.magicWord = magicWord; + } + + @Override + public List findElements(SearchContext context) { + return List.of(((FindByMagic) context).findByMagic(magicWord)); } } + public interface FindByMagic { + WebElement findByMagic(String magicWord); + } + @Test - public void shouldNotAugmentSubclassesOfRemoteWebDriver() { - Capabilities caps = new ImmutableCapabilities(); + public void shouldBeAbleToAugmentMultipleTimes() { + Capabilities caps = new ImmutableCapabilities("canRotate", true, "magic.numbers", true); + StubExecutor stubExecutor = new StubExecutor(caps); - WebDriver driver = new RemoteWebDriverSubclass(stubExecutor, caps); + stubExecutor.expect(DriverCommand.GET_SCREEN_ORIENTATION, + Collections.emptyMap(), + ScreenOrientation.PORTRAIT.name()); + RemoteWebDriver driver = new RemoteWebDriver(stubExecutor, caps); + + WebDriver augmented = getAugmenter() + .addDriverAugmentation( + "canRotate", + Rotatable.class, + (c, exe) -> new RemoteRotatable(exe)) + .augment(driver); + + assertThat(driver).isNotSameAs(augmented); + assertThat(augmented).isInstanceOf(Rotatable.class); + assertThat(augmented).isNotInstanceOf(HasMagicNumbers.class); + + WebDriver augmentedAgain = getAugmenter() + .addDriverAugmentation( + "magic.numbers", + HasMagicNumbers.class, + (c, exe) -> () -> 42) + .augment(augmented); + + assertThat(augmented).isNotSameAs(augmentedAgain); + assertThat(augmentedAgain).isInstanceOf(Rotatable.class); + assertThat(augmentedAgain).isInstanceOf(HasMagicNumbers.class); + + ((Rotatable) augmentedAgain).getOrientation(); // Should not throw. + + assertThat(((HasCapabilities) augmentedAgain).getCapabilities()) + .isSameAs(driver.getCapabilities()); + } - WebDriver augmentedDriver = getAugmenter().augment(driver); + protected static class StubExecutor implements CommandExecutor { + private final Capabilities capabilities; + private final List expected = new ArrayList<>(); - assertThat(augmentedDriver).isSameAs(driver); + protected StubExecutor(Capabilities capabilities) { + this.capabilities = capabilities; + } + + @Override + public Response execute(Command command) { + if (DriverCommand.NEW_SESSION.equals(command.getName())) { + Response response = new Response(new SessionId("foo")); + response.setValue(capabilities.asMap()); + return response; + } + + for (Data possibleMatch : expected) { + if (possibleMatch.commandName.equals(command.getName()) && + possibleMatch.args.equals(command.getParameters())) { + Response response = new Response(new SessionId("foo")); + response.setValue(possibleMatch.returnValue); + return response; + } + } + + throw new AssertionError("Unexpected method invocation: " + command); + } + + public void expect(String commandName, Map args, Object returnValue) { + expected.add(new Data(commandName, args, returnValue)); + } + + private static class Data { + public String commandName; + public Map args; + public Object returnValue; + + public Data(String commandName, Map args, Object returnValue) { + this.commandName = commandName; + this.args = args; + this.returnValue = returnValue; + } + } + } + + public interface MyInterface { + String getHelloWorld(); + } + + public static class DetonatingDriver extends RemoteWebDriver { + private Capabilities caps; + + public void setCapabilities(Capabilities caps) { + this.caps = caps; + } + + @Override + public Capabilities getCapabilities() { + return caps; + } + + @Override + public WebElement findElement(By locator) { + return super.findElement(locator); + } + + @Override + protected WebElement findElement(String by, String using) { + if ("id".equals(by)) { + throw new NoSuchElementException("Boom"); + } + return null; + } + } + + public interface HasMagicNumbers { + int getMagicNumber(); + } + + public static class ChildRemoteDriver extends RemoteWebDriver implements HasMagicNumbers { + private int magicNumber = 3; + + @Override + public Capabilities getCapabilities() { + return new FirefoxOptions(); + } + + @Override + public int getMagicNumber() { + return magicNumber; + } + } + + public static class WithFinals extends RemoteWebDriver { + public final String finalField = "FINAL"; + + @Override + public Capabilities getCapabilities() { + return new ImmutableCapabilities(); + } } } diff --git a/java/client/test/org/openqa/selenium/remote/BUILD.bazel b/java/client/test/org/openqa/selenium/remote/BUILD.bazel index 8e8ce3060462f..1464f7abb9708 100644 --- a/java/client/test/org/openqa/selenium/remote/BUILD.bazel +++ b/java/client/test/org/openqa/selenium/remote/BUILD.bazel @@ -1,26 +1,12 @@ load("@rules_jvm_external//:defs.bzl", "artifact") load("//java:defs.bzl", "java_test_suite") -java_library( - name = "BaseAugmenterTest", - srcs = glob(["BaseAugmenterTest.java"]), - deps = [ - "//java/client/src/org/openqa/selenium/firefox", - "//java/client/src/org/openqa/selenium/remote", - artifact("org.assertj:assertj-core"), - artifact("com.google.guava:guava"), - artifact("junit:junit"), - artifact("org.mockito:mockito-core"), - ], -) - java_test_suite( name = "small-tests", size = "small", srcs = glob( ["*Test.java"], exclude = [ - "BaseAugmenterTest.java", "RemoteWebDriverScreenshotTest.java", ], ), @@ -28,7 +14,6 @@ java_test_suite( "no-sandbox", ], deps = [ - ":BaseAugmenterTest", "//java/client/src/org/openqa/selenium:core", "//java/client/src/org/openqa/selenium/chrome", "//java/client/src/org/openqa/selenium/firefox", diff --git a/java/client/test/org/openqa/selenium/remote/BaseAugmenterTest.java b/java/client/test/org/openqa/selenium/remote/BaseAugmenterTest.java deleted file mode 100644 index 749621eda81ee..0000000000000 --- a/java/client/test/org/openqa/selenium/remote/BaseAugmenterTest.java +++ /dev/null @@ -1,403 +0,0 @@ -// Licensed to the Software Freedom Conservancy (SFC) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The SFC licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. 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 org.openqa.selenium.remote; - -import static java.util.Collections.singletonMap; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.Mockito.mock; -import static org.openqa.selenium.remote.DriverCommand.FIND_ELEMENT; - -import com.google.common.collect.ImmutableMap; -import org.junit.Ignore; -import org.junit.Test; -import org.openqa.selenium.By; -import org.openqa.selenium.Capabilities; -import org.openqa.selenium.HasCapabilities; -import org.openqa.selenium.ImmutableCapabilities; -import org.openqa.selenium.NoSuchElementException; -import org.openqa.selenium.Rotatable; -import org.openqa.selenium.ScreenOrientation; -import org.openqa.selenium.SearchContext; -import org.openqa.selenium.TakesScreenshot; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.firefox.FirefoxOptions; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public abstract class BaseAugmenterTest { - - @Test - public void shouldReturnANormalWebDriverUntouched() { - WebDriver driver = mock(WebDriver.class); - - WebDriver returned = getAugmenter().augment(driver); - - assertThat(returned).isSameAs(driver); - } - - @Test - public void shouldAddInterfaceFromCapabilityIfNecessary() { - final Capabilities caps = new ImmutableCapabilities("magic.numbers", true); - WebDriver driver = new RemoteWebDriver(new StubExecutor(caps), caps); - - BaseAugmenter augmenter = getAugmenter(); - augmenter.addDriverAugmentation("magic.numbers", new AddsMagicNumberHolder()); - WebDriver returned = augmenter.augment(driver); - - assertThat(returned).isNotSameAs(driver); - assertThat(returned).isInstanceOf(TakesScreenshot.class); - } - - @Test - public void shouldNotAddInterfaceWhenBooleanValueForItIsFalse() { - Capabilities caps = new ImmutableCapabilities("magic.numbers", false); - WebDriver driver = new RemoteWebDriver(new StubExecutor(caps), caps); - - BaseAugmenter augmenter = getAugmenter(); - augmenter.addDriverAugmentation("magic.numbers", new AddsMagicNumberHolder()); - WebDriver returned = augmenter.augment(driver); - - assertThat(returned).isSameAs(driver); - assertThat(returned).isNotInstanceOf(MagicNumberHolder.class); - } - - @Test - public void shouldDelegateToHandlerIfAdded() { - Capabilities caps = new ImmutableCapabilities("foo", true); - - BaseAugmenter augmenter = getAugmenter(); - augmenter.addDriverAugmentation("foo", new AugmenterProvider() { - @Override - public Class getDescribedInterface() { - return MyInterface.class; - } - - @Override - public InterfaceImplementation getImplementation(Object value) { - return (executeMethod, self, method, args) -> "Hello World"; - } - }); - - WebDriver driver = new RemoteWebDriver(new StubExecutor(caps), caps); - WebDriver returned = augmenter.augment(driver); - - String text = ((MyInterface) returned).getHelloWorld(); - assertThat(text).isEqualTo("Hello World"); - } - - @Test - public void shouldDelegateUnmatchedMethodCallsToDriverImplementation() { - Capabilities caps = new ImmutableCapabilities("magic.numbers", true); - StubExecutor stubExecutor = new StubExecutor(caps); - stubExecutor.expect(DriverCommand.GET_TITLE, new HashMap<>(), "Title"); - WebDriver driver = new RemoteWebDriver(stubExecutor, caps); - - BaseAugmenter augmenter = getAugmenter(); - augmenter.addDriverAugmentation("magic.numbers", new AddsMagicNumberHolder()); - WebDriver returned = augmenter.augment(driver); - - assertThat(returned.getTitle()).isEqualTo("Title"); - } - - @Test - public void proxyShouldNotAppearInStackTraces() { - // This will force the class to be enhanced - final Capabilities caps = new ImmutableCapabilities("magic.numbers", true); - - DetonatingDriver driver = new DetonatingDriver(); - driver.setCapabilities(caps); - - BaseAugmenter augmenter = getAugmenter(); - augmenter.addDriverAugmentation("magic.numbers", new AddsMagicNumberHolder()); - WebDriver returned = augmenter.augment(driver); - - assertThatExceptionOfType(NoSuchElementException.class) - .isThrownBy(() -> returned.findElement(By.id("ignored"))); - } - - - @Test - public void shouldLeaveAnUnAugmentableElementAlone() { - RemoteWebElement element = new RemoteWebElement(); - element.setId("1234"); - - WebElement returned = getAugmenter().augment(element); - - assertThat(returned).isSameAs(element); - } - - @Test - public void shouldAllowAnElementToBeAugmented() { - RemoteWebElement element = new RemoteWebElement(); - element.setId("1234"); - - BaseAugmenter augmenter = getAugmenter(); - augmenter.addElementAugmentation("foo", new AugmenterProvider() { - @Override - public Class getDescribedInterface() { - return MyInterface.class; - } - - @Override - public InterfaceImplementation getImplementation(Object value) { - return (executeMethod, self, method, args) -> "Hello World"; - } - }); - - final Capabilities caps = new ImmutableCapabilities("foo", true); - - StubExecutor executor = new StubExecutor(caps); - RemoteWebDriver parent = new RemoteWebDriver(executor, caps) { - @Override - public Capabilities getCapabilities() { - return caps; - } - }; - element.setParent(parent); - - WebElement returned = augmenter.augment(element); - - assertThat(returned).isInstanceOf(MyInterface.class); - - executor.expect(DriverCommand.CLICK_ELEMENT, singletonMap("id", "1234"), null); - returned.click(); - } - - @Test - public void shouldCopyFieldsFromTemplateInstanceIntoChildInstance() { - ChildRemoteDriver driver = new ChildRemoteDriver(); - driver.setMagicNumber(3); - MagicNumberHolder holder = (MagicNumberHolder) getAugmenter().augment(driver); - - assertThat(holder.getMagicNumber()).isEqualTo(3); - } - - @Test - public void shouldNotChokeOnFinalFields() { - WithFinals withFinals = new WithFinals(); - getAugmenter().augment(withFinals); - } - - @Test - @Ignore("Reflexive calls are currently broken in every implementation") - public void shouldAllowReflexiveCalls() { - Capabilities caps = new ImmutableCapabilities("find by magic", true); - StubExecutor executor = new StubExecutor(caps); - final WebElement element = mock(WebElement.class); - executor.expect( - FIND_ELEMENT, - ImmutableMap.of("using", "magic", "value", "cheese"), - element); - - WebDriver driver = new RemoteWebDriver(executor, caps); - BaseAugmenter augmenter = getAugmenter(); - - augmenter.addDriverAugmentation("find by magic", new AugmenterProvider() { - @Override - public Class getDescribedInterface() { - return FindByMagic.class; - } - - @Override - public InterfaceImplementation getImplementation(Object value) { - return (executeMethod, self, method, args) -> element; - } - }); - WebDriver returned = augmenter.augment(driver); - - returned.findElement(new ByMagic("cheese")); - // No exception is a Good Thing - } - - private static class ByMagic extends By { - private final String magicWord; - - public ByMagic(String magicWord) { - this.magicWord = magicWord; - } - - @Override - public List findElements(SearchContext context) { - return List.of(((FindByMagic) context).findByMagic(magicWord)); - } - } - - public interface FindByMagic { - WebElement findByMagic(String magicWord); - } - - @Test - public void shouldBeAbleToAugmentMultipleTimes() { - Capabilities caps = new ImmutableCapabilities("canRotate", true, "magic.numbers", true); - - StubExecutor stubExecutor = new StubExecutor(caps); - stubExecutor.expect(DriverCommand.GET_SCREEN_ORIENTATION, - Collections.emptyMap(), - ScreenOrientation.PORTRAIT.name()); - RemoteWebDriver driver = new RemoteWebDriver(stubExecutor, caps); - - BaseAugmenter augmenter = getAugmenter(); - augmenter.addDriverAugmentation("canRotate", new AddRotatable()); - - WebDriver augmented = augmenter.augment(driver); - assertThat(driver).isNotSameAs(augmented); - assertThat(augmented).isInstanceOf(Rotatable.class); - assertThat(augmented).isNotInstanceOf(MagicNumberHolder.class); - - augmenter = getAugmenter(); - augmenter.addDriverAugmentation("magic.numbers", new AddsMagicNumberHolder()); - - WebDriver augmentedAgain = augmenter.augment(augmented); - assertThat(augmented).isNotSameAs(augmentedAgain); - assertThat(augmentedAgain).isInstanceOf(Rotatable.class); - assertThat(augmentedAgain).isInstanceOf(MagicNumberHolder.class); - - ((Rotatable) augmentedAgain).getOrientation(); // Should not throw. - - assertThat(((HasCapabilities) augmentedAgain).getCapabilities()) - .isSameAs(driver.getCapabilities()); - } - - protected static class StubExecutor implements CommandExecutor { - private final Capabilities capabilities; - private final List expected = new ArrayList<>(); - - protected StubExecutor(Capabilities capabilities) { - this.capabilities = capabilities; - } - - @Override - public Response execute(Command command) { - if (DriverCommand.NEW_SESSION.equals(command.getName())) { - Response response = new Response(new SessionId("foo")); - response.setValue(capabilities.asMap()); - return response; - } - - for (Data possibleMatch : expected) { - if (possibleMatch.commandName.equals(command.getName()) && - possibleMatch.args.equals(command.getParameters())) { - Response response = new Response(new SessionId("foo")); - response.setValue(possibleMatch.returnValue); - return response; - } - } - - throw new AssertionError("Unexpected method invocation: " + command); - } - - public void expect(String commandName, Map args, Object returnValue) { - expected.add(new Data(commandName, args, returnValue)); - } - - private static class Data { - public String commandName; - public Map args; - public Object returnValue; - - public Data(String commandName, Map args, Object returnValue) { - this.commandName = commandName; - this.args = args; - this.returnValue = returnValue; - } - } - } - - public interface MyInterface { - String getHelloWorld(); - } - - public static class DetonatingDriver extends RemoteWebDriver { - private Capabilities caps; - - public void setCapabilities(Capabilities caps) { - this.caps = caps; - } - - @Override - public Capabilities getCapabilities() { - return caps; - } - - @Override - public WebElement findElement(By locator) { - return super.findElement(locator); - } - - @Override - protected WebElement findElement(String by, String using) { - if ("id".equals(by)) { - throw new NoSuchElementException("Boom"); - } - return null; - } - } - - public interface MagicNumberHolder { - int getMagicNumber(); - void setMagicNumber(int number); - } - - public static class ChildRemoteDriver extends RemoteWebDriver implements MagicNumberHolder { - private int magicNumber; - - @Override - public Capabilities getCapabilities() { - return new FirefoxOptions(); - } - - @Override - public int getMagicNumber() { - return magicNumber; - } - - @Override - public void setMagicNumber(int magicNumber) { - this.magicNumber = magicNumber; - } - } - - public static class WithFinals extends RemoteWebDriver { - public final String finalField = "FINAL"; - - @Override - public Capabilities getCapabilities() { - return new ImmutableCapabilities(); - } - } - - public abstract BaseAugmenter getAugmenter(); - - private static class AddsMagicNumberHolder implements AugmenterProvider { - @Override - public Class getDescribedInterface() { - return MagicNumberHolder.class; - } - - @Override - public InterfaceImplementation getImplementation(Object value) { - return (executeMethod, self, method, args) -> null; - } - } -} diff --git a/java/client/test/org/openqa/selenium/remote/JdkAugmenterTest.java b/java/client/test/org/openqa/selenium/remote/JdkAugmenterTest.java deleted file mode 100644 index 22ee1b9812788..0000000000000 --- a/java/client/test/org/openqa/selenium/remote/JdkAugmenterTest.java +++ /dev/null @@ -1,25 +0,0 @@ -// Licensed to the Software Freedom Conservancy (SFC) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The SFC licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. 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 org.openqa.selenium.remote; - -public class JdkAugmenterTest extends BaseAugmenterTest { - - public BaseAugmenter getAugmenter() { - return new JdkAugmenter(); - } -}