Skip to content

Commit 47ee733

Browse files
committed
[java] Documenting EventFiringDecorator, adding tests that verify the documented behavior and fixing bugs found by these tests
1 parent 1f4909f commit 47ee733

File tree

3 files changed

+547
-40
lines changed

3 files changed

+547
-40
lines changed

java/client/src/org/openqa/selenium/support/events/EventFiringDecorator.java

+186-40
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
package org.openqa.selenium.support.events;
1919

20-
import com.google.common.base.Throwables;
2120
import com.google.common.primitives.Primitives;
2221

2322
import org.openqa.selenium.Alert;
@@ -31,10 +30,136 @@
3130
import java.lang.reflect.Method;
3231
import java.util.Arrays;
3332
import java.util.List;
33+
import java.util.logging.Level;
34+
import java.util.logging.Logger;
3435

36+
/**
37+
* This decorator creates a wrapper around an arbitrary {@link WebDriver} instance that notifies
38+
* registered listeners about events happening in this WebDriver instance and related objects
39+
* ({@link WebElement}}s found by this driver, {@link Alert}s,
40+
* {@link org.openqa.selenium.WebDriver.Window}s etc).
41+
* <p>
42+
* Listeners should implement {@link WebDriverListener} interface. It supports three types of events:
43+
* <ul>
44+
* <li>"before"-event: a method is about to be called;</li>
45+
* <li>"after"-event: a method was called successfully and returned some result;</li>
46+
* <li>"error"-event: a method was called and thrown an exception.</li>
47+
* </ul>
48+
* To use this decorator you have to prepare a listener, create a decorator using this listener,
49+
* decorate the original WebDriver instance with this decorator and use the new WebDriver
50+
* instance created by decorator instead of the original one:
51+
* <code>
52+
* WebDriver original = new FirefoxDriver();
53+
* WebDriverListener listener = new MyListener();
54+
* WebDriver decorated = new EventFiringDecorator(listener).decorate(original);
55+
* decorated.get("http://example.com/");
56+
* WebElement header = decorated.findElement(By.tagName("h1"));
57+
* String headerText = header.getText();
58+
* </code>
59+
* <p>
60+
* The instance of WebDriver created by the decorator implements all the same interfaces as
61+
* the original driver.
62+
* <p>
63+
* A listener can subscribe to "specific" or "generic" events (or both). A "specific" event
64+
* correspond to a single specific method, a "generic" event correspond to any method called in
65+
* a class or in any class.
66+
* <p>
67+
* To subscribe to a "specific" event a listener should implement a method with a name build after
68+
* the target method to be watched. The listener methods for "before"-events receive the parameters
69+
* passed to the decorated method. The listener methods for "after"-events receive the parameters
70+
* passed to the decorated method as well as the result returned by this method.
71+
* <code>
72+
* WebDriverListener listener = new WebDriverListener() {
73+
* @Override
74+
* public void beforeGet(WebDriver driver, String url) {
75+
* logger.log("About to open a page %s", url);
76+
* }
77+
* @Override
78+
* public void afterGetText(String result, WebElement element) {
79+
* logger.log("Element %s has text '%s'", element, result);
80+
* }
81+
* };
82+
* </code>
83+
* <p>
84+
* To subscribe to a "generic" event a listener should implement a method with a name build after
85+
* the class to be watched:
86+
* <code>
87+
* WebDriverListener listener = new WebDriverListener() {
88+
* @Override
89+
* public void beforeAnyWebElementCall(WebElement element, Method method, Object[] args) {
90+
* logger.log("About to call a method %s in element %s with parameters %s",
91+
* method, element, args);
92+
* }
93+
* @Override
94+
* public void afterAnyWebElementCall(WebElement element, Method method, Object result, Object[] args) {
95+
* logger.log("Method %s called in element %s with parameters %s returned %s",
96+
* method, element, args, result);
97+
* }
98+
* };
99+
* </code>
100+
* <p>
101+
* There are also listener methods for "super-generic" events:
102+
* <code>
103+
* WebDriverListener listener = new WebDriverListener() {
104+
* @Override
105+
* public void beforeAnyCall(Object target, Method method, Object[] args) {
106+
* logger.log("About to call a method %s in %s with parameters %s",
107+
* method, target, args);
108+
* }
109+
* @Override
110+
* public void afterAnyCall(Object target, Method method, Object result, Object[] args) {
111+
* logger.log("Method %s called in %s with parameters %s returned %s",
112+
* method, target, args, result);
113+
* }
114+
* };
115+
* </code>
116+
* <p>
117+
* A listener can subscribe to both "specific" and "generic" events at the same time. In this case
118+
* "before"-events are fired in order from the most generic to the most specific,
119+
* and "after"-events are generated in the opposite order, for example:
120+
* <code>
121+
* beforeAnyCall
122+
* beforeAnyWebDriverCall
123+
* beforeGet
124+
* // the actual call to the decorated method here
125+
* afterGet
126+
* afterAnyWebDriverCall
127+
* afterAnyCall
128+
* </code>
129+
* <p>
130+
* One of the most obvious use of this decorator is logging. But it can be used to modify behavior
131+
* of the original driver to some extent because listener methods are executed in the same thread
132+
* as the original driver methods.
133+
* <p>
134+
* For example, a listener can be used to slow down execution for demonstration purposes, just
135+
* make a listener that adds a pause before some operations:
136+
* <code>
137+
* WebDriverListener listener = new WebDriverListener() {
138+
* @Override
139+
* public void beforeClick(WebElement element) {
140+
* try {
141+
* Thread.sleep(3000);
142+
* } catch (InterruptedException e) {
143+
* Thread.currentThread().interrupt();
144+
* }
145+
* }
146+
* };
147+
* </code>
148+
* <p>
149+
* Just be careful to not block the current thread in a listener method!
150+
* <p>
151+
* Actually, listeners can't affect driver behavior too much. They can't throw any exceptions
152+
* (they can, but the decorator suppresses these exceptions), can't prevent execution of
153+
* the decorated methods, can't modify parameters and results of the methods.
154+
* <p>
155+
* Decorators that modify the behaviour of the underlying drivers should be implemented by
156+
* extending {@link WebDriverDecorator}, not by creating sophisticated listeners.
157+
*/
35158
@Beta
36159
public class EventFiringDecorator extends WebDriverDecorator {
37160

161+
private static final Logger logger = Logger.getLogger(EventFiringDecorator.class.getName());
162+
38163
private final List<WebDriverListener> listeners;
39164

40165
public EventFiringDecorator(WebDriverListener... listeners) {
@@ -55,26 +180,41 @@ public void afterCallGlobal(Decorated<?> target, Method method, Object result, O
55180

56181
@Override
57182
public Object onErrorGlobal(Decorated<?> target, Method method, InvocationTargetException e, Object[] args) throws Throwable {
58-
listeners.forEach(listener -> listener.onError(target.getOriginal(), method, e, args));
183+
listeners.forEach(listener -> {
184+
try {
185+
listener.onError(target.getOriginal(), method, e, args);
186+
} catch (Throwable t) {
187+
logger.log(Level.WARNING, t.getMessage(), t);
188+
}
189+
});
59190
return super.onErrorGlobal(target, method, e, args);
60191
}
61192

62193
private void fireBeforeEvents(WebDriverListener listener, Decorated<?> target, Method method, Object[] args) {
63-
listener.beforeAnyCall(target.getOriginal(), method, args);
64-
if (target.getOriginal() instanceof WebDriver) {
65-
listener.beforeAnyWebDriverCall((WebDriver) target.getOriginal(), method, args);
66-
} else if (target.getOriginal() instanceof WebElement) {
67-
listener.beforeAnyWebElementCall((WebElement) target.getOriginal(), method, args);
68-
} else if (target.getOriginal() instanceof WebDriver.Navigation) {
69-
listener.beforeAnyNavigationCall((WebDriver.Navigation) target.getOriginal(), method, args);
70-
} else if (target.getOriginal() instanceof Alert) {
71-
listener.beforeAnyAlertCall((Alert) target.getOriginal(), method, args);
72-
} else if (target.getOriginal() instanceof WebDriver.Options) {
73-
listener.beforeAnyOptionsCall((WebDriver.Options) target.getOriginal(), method, args);
74-
} else if (target.getOriginal() instanceof WebDriver.Timeouts) {
75-
listener.beforeAnyTimeoutsCall((WebDriver.Timeouts) target.getOriginal(), method, args);
76-
} else if (target.getOriginal() instanceof WebDriver.Window) {
77-
listener.beforeAnyWindowCall((WebDriver.Window) target.getOriginal(), method, args);
194+
try {
195+
listener.beforeAnyCall(target.getOriginal(), method, args);
196+
} catch (Throwable t) {
197+
logger.log(Level.WARNING, t.getMessage(), t);
198+
}
199+
200+
try {
201+
if (target.getOriginal() instanceof WebDriver) {
202+
listener.beforeAnyWebDriverCall((WebDriver) target.getOriginal(), method, args);
203+
} else if (target.getOriginal() instanceof WebElement) {
204+
listener.beforeAnyWebElementCall((WebElement) target.getOriginal(), method, args);
205+
} else if (target.getOriginal() instanceof WebDriver.Navigation) {
206+
listener.beforeAnyNavigationCall((WebDriver.Navigation) target.getOriginal(), method, args);
207+
} else if (target.getOriginal() instanceof Alert) {
208+
listener.beforeAnyAlertCall((Alert) target.getOriginal(), method, args);
209+
} else if (target.getOriginal() instanceof WebDriver.Options) {
210+
listener.beforeAnyOptionsCall((WebDriver.Options) target.getOriginal(), method, args);
211+
} else if (target.getOriginal() instanceof WebDriver.Timeouts) {
212+
listener.beforeAnyTimeoutsCall((WebDriver.Timeouts) target.getOriginal(), method, args);
213+
} else if (target.getOriginal() instanceof WebDriver.Window) {
214+
listener.beforeAnyWindowCall((WebDriver.Window) target.getOriginal(), method, args);
215+
}
216+
} catch (Throwable t) {
217+
logger.log(Level.WARNING, t.getMessage(), t);
78218
}
79219

80220
String methodName = createEventMethodName("before", method.getName());
@@ -93,23 +233,6 @@ private void fireBeforeEvents(WebDriverListener listener, Decorated<?> target, M
93233
}
94234

95235
private void fireAfterEvents(WebDriverListener listener, Decorated<?> target, Method method, Object res, Object[] args) {
96-
listener.afterAnyCall(target.getOriginal(), method, res, args);
97-
if (target.getOriginal() instanceof WebDriver) {
98-
listener.afterAnyWebDriverCall((WebDriver) target.getOriginal(), method, res, args);
99-
} else if (target.getOriginal() instanceof WebElement) {
100-
listener.afterAnyWebElementCall((WebElement) target.getOriginal(), method, res, args);
101-
} else if (target.getOriginal() instanceof WebDriver.Navigation) {
102-
listener.afterAnyNavigationCall((WebDriver.Navigation) target.getOriginal(), method, res, args);
103-
} else if (target.getOriginal() instanceof Alert) {
104-
listener.afterAnyAlertCall((Alert) target.getOriginal(), method, res, args);
105-
} else if (target.getOriginal() instanceof WebDriver.Options) {
106-
listener.afterAnyOptionsCall((WebDriver.Options) target.getOriginal(), method, res, args);
107-
} else if (target.getOriginal() instanceof WebDriver.Timeouts) {
108-
listener.afterAnyTimeoutsCall((WebDriver.Timeouts) target.getOriginal(), method, res, args);
109-
} else if (target.getOriginal() instanceof WebDriver.Window) {
110-
listener.afterAnyWindowCall((WebDriver.Window) target.getOriginal(), method, res, args);
111-
}
112-
113236
String methodName = createEventMethodName("after", method.getName());
114237

115238
boolean isVoid = method.getReturnType() == Void.TYPE
@@ -130,6 +253,32 @@ private void fireAfterEvents(WebDriverListener listener, Decorated<?> target, Me
130253
if (m != null) {
131254
callListenerMethod(m, listener, args2);
132255
}
256+
257+
try {
258+
if (target.getOriginal() instanceof WebDriver) {
259+
listener.afterAnyWebDriverCall((WebDriver) target.getOriginal(), method, res, args);
260+
} else if (target.getOriginal() instanceof WebElement) {
261+
listener.afterAnyWebElementCall((WebElement) target.getOriginal(), method, res, args);
262+
} else if (target.getOriginal() instanceof WebDriver.Navigation) {
263+
listener.afterAnyNavigationCall((WebDriver.Navigation) target.getOriginal(), method, res, args);
264+
} else if (target.getOriginal() instanceof Alert) {
265+
listener.afterAnyAlertCall((Alert) target.getOriginal(), method, res, args);
266+
} else if (target.getOriginal() instanceof WebDriver.Options) {
267+
listener.afterAnyOptionsCall((WebDriver.Options) target.getOriginal(), method, res, args);
268+
} else if (target.getOriginal() instanceof WebDriver.Timeouts) {
269+
listener.afterAnyTimeoutsCall((WebDriver.Timeouts) target.getOriginal(), method, res, args);
270+
} else if (target.getOriginal() instanceof WebDriver.Window) {
271+
listener.afterAnyWindowCall((WebDriver.Window) target.getOriginal(), method, res, args);
272+
}
273+
} catch (Throwable t) {
274+
logger.log(Level.WARNING, t.getMessage(), t);
275+
}
276+
277+
try {
278+
listener.afterAnyCall(target.getOriginal(), method, res, args);
279+
} catch (Throwable t) {
280+
logger.log(Level.WARNING, t.getMessage(), t);
281+
}
133282
}
134283

135284
private String createEventMethodName(String prefix, String originalMethodName) {
@@ -151,7 +300,7 @@ private boolean parametersMatch(Method m, Object[] args) {
151300
return false;
152301
}
153302
for (int i = 0; i < params.length; i++) {
154-
if (! Primitives.wrap(params[i]).isAssignableFrom(args[i].getClass())) {
303+
if (args[i] != null && ! Primitives.wrap(params[i]).isAssignableFrom(args[i].getClass())) {
155304
return false;
156305
}
157306
}
@@ -161,11 +310,8 @@ private boolean parametersMatch(Method m, Object[] args) {
161310
private void callListenerMethod(Method m, WebDriverListener listener, Object[] args) {
162311
try {
163312
m.invoke(listener, args);
164-
} catch (IllegalAccessException e) {
165-
throw new RuntimeException(e);
166-
} catch (InvocationTargetException e) {
167-
Throwables.throwIfUnchecked(e.getCause());
168-
throw new RuntimeException(e.getCause());
313+
} catch (Throwable t) {
314+
logger.log(Level.WARNING, t.getMessage(), t);
169315
}
170316
}
171317
}

java/client/src/org/openqa/selenium/support/events/WebDriverListener.java

+6
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@
3535
import java.util.List;
3636
import java.util.Set;
3737

38+
/**
39+
* Classes that implement this interface are intended to be used with {@link EventFiringDecorator},
40+
* read documentation for this class to find detailed usage description.
41+
* <p>
42+
* This interface provides empty default implementation for all methods that does nothing.
43+
*/
3844
@Beta
3945
public interface WebDriverListener {
4046

0 commit comments

Comments
 (0)