Skip to content

Commit

Permalink
[rb] Implement High Level Logging API with BiDi (#14073)
Browse files Browse the repository at this point in the history
* add and remove logging handlers with BiDi

* use #object_id instead of creating new ids to track callbacks

* do not send browsing contexts to subscription if not needed

* deprecate Driver#script & LogInspector

* error if trying to remove an id that does not exist

* do not unsubscribe if never subscribed in the first place

* do not deprecate callbacks people don't have to care about getting the id back
  • Loading branch information
titusfortner authored Jun 5, 2024
1 parent e672104 commit 8ac19e4
Show file tree
Hide file tree
Showing 14 changed files with 350 additions and 38 deletions.
10 changes: 10 additions & 0 deletions rb/lib/selenium/webdriver/bidi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ module WebDriver
class BiDi
autoload :Session, 'selenium/webdriver/bidi/session'
autoload :LogInspector, 'selenium/webdriver/bidi/log_inspector'
autoload :LogHandler, 'selenium/webdriver/bidi/log_handler'
autoload :BrowsingContext, 'selenium/webdriver/bidi/browsing_context'
autoload :Struct, 'selenium/webdriver/bidi/struct'

def initialize(url:)
@ws = WebSocketConnection.new(url: url)
Expand All @@ -36,6 +38,14 @@ def callbacks
@ws.callbacks
end

def add_callback(event, &block)
@ws.add_callback(event, &block)
end

def remove_callback(event, id)
@ws.remove_callback(event, id)
end

def session
@session ||= Session.new(self)
end
Expand Down
63 changes: 63 additions & 0 deletions rb/lib/selenium/webdriver/bidi/log_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# frozen_string_literal: true

# 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.

module Selenium
module WebDriver
class BiDi
class LogHandler
ConsoleLogEntry = BiDi::Struct.new(:level, :text, :timestamp, :method, :args, :type)
JavaScriptLogEntry = BiDi::Struct.new(:level, :text, :timestamp, :stack_trace, :type)

def initialize(bidi)
@bidi = bidi
@log_entry_subscribed = false
end

# @return [int] id of the handler
def add_message_handler(type)
subscribe_log_entry unless @log_entry_subscribed
@bidi.add_callback('log.entryAdded') do |params|
if params['type'] == type
log_entry_klass = type == 'console' ? ConsoleLogEntry : JavaScriptLogEntry
yield(log_entry_klass.new(**params))
end
end
end

# @param [int] id of the handler previously added
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
end # LogHandler
end # Bidi
end # WebDriver
end # Selenium
6 changes: 5 additions & 1 deletion rb/lib/selenium/webdriver/bidi/log_inspector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ class LogInspector
}.freeze

def initialize(driver, browsing_context_ids = nil)
WebDriver.logger.deprecate('LogInspector class',
'Script class with driver.script',
id: :log_inspector)

unless driver.capabilities.web_socket_url
raise Error::WebDriverError,
'WebDriver instance must support BiDi protocol'
Expand Down Expand Up @@ -92,7 +96,7 @@ def on_log(filter_by = nil, &block)

def on(event, &block)
event = EVENTS[event] if event.is_a?(Symbol)
@bidi.callbacks["log.#{event}"] << block
@bidi.add_callback("log.#{event}", &block)
end

def check_valid_filter(filter_by)
Expand Down
14 changes: 7 additions & 7 deletions rb/lib/selenium/webdriver/bidi/session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,21 @@ def initialize(bidi)

def status
status = @bidi.send_cmd('session.status')
Status.new(status['ready'], status['message'])
Status.new(**status)
end

def subscribe(events, browsing_contexts = nil)
events_list = Array(events)
browsing_contexts_list = browsing_contexts.nil? ? nil : Array(browsing_contexts)
opts = {events: Array(events)}
opts[:browsing_contexts] = Array(browsing_contexts) if browsing_contexts

@bidi.send_cmd('session.subscribe', events: events_list, contexts: browsing_contexts_list)
@bidi.send_cmd('session.subscribe', **opts)
end

def unsubscribe(events, browsing_contexts = nil)
events_list = Array(events)
browsing_contexts_list = browsing_contexts.nil? ? nil : Array(browsing_contexts)
opts = {events: Array(events)}
opts[:browsing_contexts] = Array(browsing_contexts) if browsing_contexts

@bidi.send_cmd('session.unsubscribe', events: events_list, contexts: browsing_contexts_list)
@bidi.send_cmd('session.unsubscribe', **opts)
end
end # Session
end # BiDi
Expand Down
40 changes: 40 additions & 0 deletions rb/lib/selenium/webdriver/bidi/struct.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

# 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.

module Selenium
module WebDriver
class BiDi
class Struct < ::Struct
def self.new(*args, &block)
super(*args) do
define_method(:initialize) do |**kwargs|
converted_kwargs = kwargs.transform_keys { |key| camel_to_snake(key.to_s).to_sym }
super(*converted_kwargs.values_at(*self.class.members))
end
class_eval(&block) if block
end
end

def camel_to_snake(camel_str)
camel_str.gsub(/([A-Z])/, '_\1').downcase
end
end
end # BiDi
end # WebDriver
end # Selenium
1 change: 1 addition & 0 deletions rb/lib/selenium/webdriver/common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,4 @@
require 'selenium/webdriver/common/shadow_root'
require 'selenium/webdriver/common/websocket_connection'
require 'selenium/webdriver/common/child_process'
require 'selenium/webdriver/common/script'
22 changes: 16 additions & 6 deletions rb/lib/selenium/webdriver/common/driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,22 @@ def navigate
@navigate ||= WebDriver::Navigation.new(bridge)
end

#
# @return [Script]
# @see Script
#

def script(*args)
if args.any?
WebDriver.logger.deprecate('`Driver#script` as an alias for `#execute_script`',
'`Driver#execute_script`',
id: :driver_script)
execute_script(*args)
else
@script ||= WebDriver::Script.new(bridge)
end
end

#
# @return [TargetLocator]
# @see TargetLocator
Expand Down Expand Up @@ -262,12 +278,6 @@ def add_virtual_authenticator(options)

alias all find_elements

#
# driver.script('function() { ... };')
#

alias script execute_script

# Get the first element matching the given selector. If given a
# String or Symbol, it will be used as the id of the element.
#
Expand Down
45 changes: 45 additions & 0 deletions rb/lib/selenium/webdriver/common/script.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

# 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.

module Selenium
module WebDriver
class Script
def initialize(bridge)
@log_handler = BiDi::LogHandler.new(bridge.bidi)
end

# @return [int] id of the handler
def add_console_message_handler(&block)
@log_handler.add_message_handler('console', &block)
end

# @return [int] id of the handler
def add_javascript_error_handler(&block)
@log_handler.add_message_handler('javascript', &block)
end

# @param [int] id of the handler previously added
def remove_console_message_handler(id)
@log_handler.remove_message_handler(id)
end

alias remove_javascript_error_handler remove_console_message_handler
end # Script
end # WebDriver
end # Selenium
12 changes: 12 additions & 0 deletions rb/lib/selenium/webdriver/common/websocket_connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,18 @@ def callbacks
@callbacks ||= Hash.new { |callbacks, event| callbacks[event] = [] }
end

def add_callback(event, &block)
callbacks[event] << block
block.object_id
end

def remove_callback(event, id)
return if callbacks[event].reject! { |callback| callback.object_id == id }

ids = callbacks[event]&.map(&:object_id)
raise Error::WebDriverError, "Callback with ID #{id} does not exist for event #{event}: #{ids}"
end

def send_cmd(**payload)
id = next_id
data = payload.merge(id: id)
Expand Down
1 change: 1 addition & 0 deletions rb/sig/lib/selenium/webdriver/common/driver.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module Selenium
@devtools: untyped
@navigate: untyped

@script: untyped
@service_manager: untyped

def self.for: (untyped browser, Hash[untyped, untyped] opts) -> untyped
Expand Down
20 changes: 20 additions & 0 deletions rb/sig/selenium/web_driver/script.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module Selenium
module WebDriver
class Script
@bidi: BiDi
@log_entry_subscribed: bool

def add_console_message_handler: -> untyped

def add_javascript_error_handler: -> untyped

def remove_console_message_handler: -> untyped

alias remove_javascript_error_handler remove_console_message_handler

private

def subscribe_log_entry: -> untyped
end
end
end
Loading

0 comments on commit 8ac19e4

Please sign in to comment.