-
-
Notifications
You must be signed in to change notification settings - Fork 8.6k
Description
Feature and motivation
I’ve started playing around with the BiDi support in the Ruby webdriver, and have noticed that subscribing to and unsubscribing from events is fairly complicated: you need to keep track of the event callbacks that have been added to the session (including by other actors) so you know whether to send a session.subscribe/session.unsubscribe command alongside adding/removing your callback.
For example, the Ruby LogHandler does this:
selenium/rb/lib/selenium/webdriver/bidi/log_handler.rb
Lines 44 to 59 in 164bf79
| def remove_message_handler(id) | |
| @bidi.remove_callback('log.entryAdded', id) | |
| unsubscribe_log_entry if @log_entry_subscribed && @bidi.callbacks['log.entryAdded'].empty? | |
| end | |
| private | |
| def subscribe_log_entry | |
| @bidi.session.subscribe('log.entryAdded') | |
| @log_entry_subscribed = true | |
| end | |
| def unsubscribe_log_entry | |
| @bidi.session.unsubscribe('log.entryAdded') | |
| @log_entry_subscribed = false | |
| end |
The Python source follows a similar pattern in Script:
selenium/py/selenium/webdriver/common/bidi/script.py
Lines 38 to 52 in 164bf79
| def remove_console_message_handler(self, id): | |
| self.conn.remove_callback(LogEntryAdded, id) | |
| self._unsubscribe_from_log_entries() | |
| remove_javascript_error_handler = remove_console_message_handler | |
| def _subscribe_to_log_entries(self): | |
| if not self.log_entry_subscribed: | |
| self.conn.execute(session_subscribe(LogEntryAdded.event_class)) | |
| self.log_entry_subscribed = True | |
| def _unsubscribe_from_log_entries(self): | |
| if self.log_entry_subscribed and LogEntryAdded.event_class not in self.conn.callbacks: | |
| self.conn.execute(session_unsubscribe(LogEntryAdded.event_class)) | |
| self.log_entry_subscribed = False |
I don’t know if this is a pattern that has crept in or a standard design/set of interfaces that are expected to exist across all the languages. Either way, having the subscriber need to keep track of who else might be subscribed seems like a problem — it adds a bit of complexity everywhere events are used and it’s easy to mess up if a user needs to listen to events that aren’t currently handled by a higher-level wrapper.
It would be really helpful if there were a subscribe or listen_to_event or some similar method (plus the corresponding unsubscribe method) on the low-level BiDi API that adds your callback and calls session.subscribe/session.unsubscribe as appropriate, e.g:
module Selenium
module WebDriver
class BiDi
# This matches what `Script` does, but I imagine a more robust version of
# this might also want to take an optional list of contexts and
# subscribe/unsubscribe only the appropriate contexts. Then the handling
# of callbacks might also filter based on what context the event is
# coming from, rather than just hooking your callback directly up to the
# @ws instance.
def add_callback(event, &block)
session.subscribe(event) if callbacks[event].empty?
@ws.add_callback(event, &block)
end
def remove_callback(event, id)
@ws.remove_callback(event, id)
session.subscribe(event) if callbacks[event].empty?
end(BiDi seems like the obvious place to put this level of abstraction in Ruby, but the Python library currently has nothing sitting in-between the higher-level APIs like Script and the actual websocket connection. I have not looked at the other language packages.)
Usage example
Currently, subscribing/unsubscribing to BiDi events requires code like (this is Ruby, but it’s similar in Python and maybe other languages):
# Subscribe to navigation events:
driver_instance.bidi.session.subscribe('browsingContext.fragmentNavigated')
callback_id = driver_instance.bidi.add_callback('browsingContext.fragmentNavigated') do |params|
# Do something with the event.
puts "Fragment Navigated: #{params}"
end
# Do some work...
# Unsubscribe:
driver_instance.bidi.remove_callback('browsingContext.fragmentNavigated', callback_id)
driver_instance.bidi.session.unsubscribe('browsingContext.fragmentNavigated') if driver_instance.bidi.callbacks['browsingContext.fragmentNavigated'].empty?Ideally, subscribing/unsubscribing would be one-liners without any conditionals:
callback_id = driver_instance.bidi.subscribe('browsingContext.fragmentNavigated') do |params|
# Do something with the event.
puts "Fragment Navigated: #{params}"
end
# Do some work...
# Unsubscribe:
driver_instance.bidi.unsubscribe('browsingContext.fragmentNavigated', callback_id)Even nicer if allows filtering by context, but maybe not as critical?
callback_id = driver_instance.bidi.subscribe('browsingContext.fragmentNavigated', contexts: [a_context]) do |params|
# Do something with the event.
puts "Fragment Navigated: #{params}"
end
# Do some work...
# Unsubscribe (from which context is handled for you based on callback_id):
driver_instance.bidi.unsubscribe('browsingContext.fragmentNavigated', callback_id)