-
-
Notifications
You must be signed in to change notification settings - Fork 8.6k
Description
Description
As stated in EventFiringDecorator javadocs:
Listeners can't affect driver behavior too much. They can't throw any exceptions (they can, but the decorator suppresses these exceptions)
There could be scenarios where a listener should actually be able to break the execution, and I think the decision of suppressing or allowing exceptions to be thrown should be left to the user, rather than imposed internally.
Context
I'm implementing Visual Regression Testing in giulong/spectrum#447, for which:
- the user configures events to listen to, let's say
afterClick
. - the first execution of each test takes a screenshot after each click. These act as snapshots.
- the subsequent executions of the same tests will take screenshots in the same moments, and compare them to the snapshots. If they don't match, an exception is thrown.
To achieve this, a decorated WebDriver
listens to the events configured by the user (afterClick
). The associated WebDriverListener
intercepts those events, and uses the original undecorated driver instance to take a screenshot. Using the undecorated driver is needed because taking the screenshot is an operation performed under the hood by the framework, and must not interfere with user interactions. So, no WebDriverListener
is actually used to intercept these screenshots being taken. To summarize:
- a decorated
WebDriver
is used to run tests. - a
WebDriverListener
interceptsafterClick
. - the undecorated driver is used to take a screenshot.
- If the screenshot doesn't match with the corresponding snapshot, an exception is thrown.
- 💥 issue: the user just gets a warning logged, instead of having the test failing fast.
Proposal
Let users decide whether to ignore exceptions or not, by adding a method to WebDriverListener
to configure the behavior of each listener:
default boolean throwsExceptions() {
return false;
}
Considerations:
-
having this at listener level instead of directly into
EventFiringDecorator
allows for a more fine grained configuration: we can have many listeners attached to the same decorator, with only a subset actually rethrowing exceptions. -
returning
false
by default means no breaking change is introduced. Whenever an exception is thrown in a listener execution, only if that listener is configured to throw exceptions, theEventFiringDecorator
will rethrow the original one:private void callListenerMethod(Method m, WebDriverListener listener, Object[] args) { try { m.invoke(listener, args); } catch (Throwable t) { LOG.log(Level.WARNING, t.getMessage(), t); // This is the new part if (listener.throwsExceptions()) { throw new WebDriverListenerException(m.getName(), t); } } }
-
WebDriverListenerException
is aRuntimeException
to avoid introducing breaking changes.
Have you considered any alternatives or workarounds?
-
I tried extending
EventFiringDecorator
but its current design, with basically just private methods such asfireBeforeEvents
andfireAfterEvents
that do all the work including suppressing exceptions, does not allow me to effectively hook onto the listeners execution logic. -
I also tried to override the
onError
method in my own decorator, but I have no access tolisteners
, which is a private field with no getter. Moreover,onError
is not triggered in such cases. So, with regard to the PR attached, I tried another approach as well, that is programmatically triggeringonError
like this:private void callListenerMethod(Method m, WebDriverListener listener, Object[] args) throws Throwable { try { m.invoke(listener, args); } catch (Throwable t) { LOG.log(Level.WARNING, t.getMessage(), t); // These lines added if (t instanceof InvocationTargetException) { this.onError(getDecoratedDriver(), m, args, (InvocationTargetException) t); } } }
this triggers
onError
method on each listener in turn:@Override public Object onError( Decorated<?> target, Method method, Object[] args, InvocationTargetException throws Throwable { listeners.forEach( listener -> { try { listener.onError(target.getOriginal(), method, args, e); } catch (Throwable t) { LOG.log(Level.WARNING, t.getMessage(), t); } }); return super.onError(target, method, args, e); }
This works, but it's a breaking change since we're always throwing exceptions. Not to mention that I don't think this is the way
onError
is intended to be used.
The only workaround I see would be to copy the entire code of EventFiringDecorator
in an own-written decorator that just doesn't swallow exceptions. But I'd like to avoid that and keep using the internal one, since it's already doing all the work.
Of course, in case I'm missing something and there's any other smart way of achieving this, just tell me.