Skip to content

Commit 8ac19e4

Browse files
authored
[rb] Implement High Level Logging API with BiDi (#14073)
* 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
1 parent e672104 commit 8ac19e4

File tree

14 files changed

+350
-38
lines changed

14 files changed

+350
-38
lines changed

rb/lib/selenium/webdriver/bidi.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ module WebDriver
2222
class BiDi
2323
autoload :Session, 'selenium/webdriver/bidi/session'
2424
autoload :LogInspector, 'selenium/webdriver/bidi/log_inspector'
25+
autoload :LogHandler, 'selenium/webdriver/bidi/log_handler'
2526
autoload :BrowsingContext, 'selenium/webdriver/bidi/browsing_context'
27+
autoload :Struct, 'selenium/webdriver/bidi/struct'
2628

2729
def initialize(url:)
2830
@ws = WebSocketConnection.new(url: url)
@@ -36,6 +38,14 @@ def callbacks
3638
@ws.callbacks
3739
end
3840

41+
def add_callback(event, &block)
42+
@ws.add_callback(event, &block)
43+
end
44+
45+
def remove_callback(event, id)
46+
@ws.remove_callback(event, id)
47+
end
48+
3949
def session
4050
@session ||= Session.new(self)
4151
end
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# frozen_string_literal: true
2+
3+
# Licensed to the Software Freedom Conservancy (SFC) under one
4+
# or more contributor license agreements. See the NOTICE file
5+
# distributed with this work for additional information
6+
# regarding copyright ownership. The SFC licenses this file
7+
# to you under the Apache License, Version 2.0 (the
8+
# "License"); you may not use this file except in compliance
9+
# with the License. You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing,
14+
# software distributed under the License is distributed on an
15+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
# KIND, either express or implied. See the License for the
17+
# specific language governing permissions and limitations
18+
# under the License.
19+
20+
module Selenium
21+
module WebDriver
22+
class BiDi
23+
class LogHandler
24+
ConsoleLogEntry = BiDi::Struct.new(:level, :text, :timestamp, :method, :args, :type)
25+
JavaScriptLogEntry = BiDi::Struct.new(:level, :text, :timestamp, :stack_trace, :type)
26+
27+
def initialize(bidi)
28+
@bidi = bidi
29+
@log_entry_subscribed = false
30+
end
31+
32+
# @return [int] id of the handler
33+
def add_message_handler(type)
34+
subscribe_log_entry unless @log_entry_subscribed
35+
@bidi.add_callback('log.entryAdded') do |params|
36+
if params['type'] == type
37+
log_entry_klass = type == 'console' ? ConsoleLogEntry : JavaScriptLogEntry
38+
yield(log_entry_klass.new(**params))
39+
end
40+
end
41+
end
42+
43+
# @param [int] id of the handler previously added
44+
def remove_message_handler(id)
45+
@bidi.remove_callback('log.entryAdded', id)
46+
unsubscribe_log_entry if @log_entry_subscribed && @bidi.callbacks['log.entryAdded'].empty?
47+
end
48+
49+
private
50+
51+
def subscribe_log_entry
52+
@bidi.session.subscribe('log.entryAdded')
53+
@log_entry_subscribed = true
54+
end
55+
56+
def unsubscribe_log_entry
57+
@bidi.session.unsubscribe('log.entryAdded')
58+
@log_entry_subscribed = false
59+
end
60+
end # LogHandler
61+
end # Bidi
62+
end # WebDriver
63+
end # Selenium

rb/lib/selenium/webdriver/bidi/log_inspector.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ class LogInspector
4141
}.freeze
4242

4343
def initialize(driver, browsing_context_ids = nil)
44+
WebDriver.logger.deprecate('LogInspector class',
45+
'Script class with driver.script',
46+
id: :log_inspector)
47+
4448
unless driver.capabilities.web_socket_url
4549
raise Error::WebDriverError,
4650
'WebDriver instance must support BiDi protocol'
@@ -92,7 +96,7 @@ def on_log(filter_by = nil, &block)
9296

9397
def on(event, &block)
9498
event = EVENTS[event] if event.is_a?(Symbol)
95-
@bidi.callbacks["log.#{event}"] << block
99+
@bidi.add_callback("log.#{event}", &block)
96100
end
97101

98102
def check_valid_filter(filter_by)

rb/lib/selenium/webdriver/bidi/session.rb

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,21 @@ def initialize(bidi)
2929

3030
def status
3131
status = @bidi.send_cmd('session.status')
32-
Status.new(status['ready'], status['message'])
32+
Status.new(**status)
3333
end
3434

3535
def subscribe(events, browsing_contexts = nil)
36-
events_list = Array(events)
37-
browsing_contexts_list = browsing_contexts.nil? ? nil : Array(browsing_contexts)
36+
opts = {events: Array(events)}
37+
opts[:browsing_contexts] = Array(browsing_contexts) if browsing_contexts
3838

39-
@bidi.send_cmd('session.subscribe', events: events_list, contexts: browsing_contexts_list)
39+
@bidi.send_cmd('session.subscribe', **opts)
4040
end
4141

4242
def unsubscribe(events, browsing_contexts = nil)
43-
events_list = Array(events)
44-
browsing_contexts_list = browsing_contexts.nil? ? nil : Array(browsing_contexts)
43+
opts = {events: Array(events)}
44+
opts[:browsing_contexts] = Array(browsing_contexts) if browsing_contexts
4545

46-
@bidi.send_cmd('session.unsubscribe', events: events_list, contexts: browsing_contexts_list)
46+
@bidi.send_cmd('session.unsubscribe', **opts)
4747
end
4848
end # Session
4949
end # BiDi
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# frozen_string_literal: true
2+
3+
# Licensed to the Software Freedom Conservancy (SFC) under one
4+
# or more contributor license agreements. See the NOTICE file
5+
# distributed with this work for additional information
6+
# regarding copyright ownership. The SFC licenses this file
7+
# to you under the Apache License, Version 2.0 (the
8+
# "License"); you may not use this file except in compliance
9+
# with the License. You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing,
14+
# software distributed under the License is distributed on an
15+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
# KIND, either express or implied. See the License for the
17+
# specific language governing permissions and limitations
18+
# under the License.
19+
20+
module Selenium
21+
module WebDriver
22+
class BiDi
23+
class Struct < ::Struct
24+
def self.new(*args, &block)
25+
super(*args) do
26+
define_method(:initialize) do |**kwargs|
27+
converted_kwargs = kwargs.transform_keys { |key| camel_to_snake(key.to_s).to_sym }
28+
super(*converted_kwargs.values_at(*self.class.members))
29+
end
30+
class_eval(&block) if block
31+
end
32+
end
33+
34+
def camel_to_snake(camel_str)
35+
camel_str.gsub(/([A-Z])/, '_\1').downcase
36+
end
37+
end
38+
end # BiDi
39+
end # WebDriver
40+
end # Selenium

rb/lib/selenium/webdriver/common.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,4 @@
9898
require 'selenium/webdriver/common/shadow_root'
9999
require 'selenium/webdriver/common/websocket_connection'
100100
require 'selenium/webdriver/common/child_process'
101+
require 'selenium/webdriver/common/script'

rb/lib/selenium/webdriver/common/driver.rb

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,22 @@ def navigate
9999
@navigate ||= WebDriver::Navigation.new(bridge)
100100
end
101101

102+
#
103+
# @return [Script]
104+
# @see Script
105+
#
106+
107+
def script(*args)
108+
if args.any?
109+
WebDriver.logger.deprecate('`Driver#script` as an alias for `#execute_script`',
110+
'`Driver#execute_script`',
111+
id: :driver_script)
112+
execute_script(*args)
113+
else
114+
@script ||= WebDriver::Script.new(bridge)
115+
end
116+
end
117+
102118
#
103119
# @return [TargetLocator]
104120
# @see TargetLocator
@@ -262,12 +278,6 @@ def add_virtual_authenticator(options)
262278

263279
alias all find_elements
264280

265-
#
266-
# driver.script('function() { ... };')
267-
#
268-
269-
alias script execute_script
270-
271281
# Get the first element matching the given selector. If given a
272282
# String or Symbol, it will be used as the id of the element.
273283
#
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# frozen_string_literal: true
2+
3+
# Licensed to the Software Freedom Conservancy (SFC) under one
4+
# or more contributor license agreements. See the NOTICE file
5+
# distributed with this work for additional information
6+
# regarding copyright ownership. The SFC licenses this file
7+
# to you under the Apache License, Version 2.0 (the
8+
# "License"); you may not use this file except in compliance
9+
# with the License. You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing,
14+
# software distributed under the License is distributed on an
15+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
# KIND, either express or implied. See the License for the
17+
# specific language governing permissions and limitations
18+
# under the License.
19+
20+
module Selenium
21+
module WebDriver
22+
class Script
23+
def initialize(bridge)
24+
@log_handler = BiDi::LogHandler.new(bridge.bidi)
25+
end
26+
27+
# @return [int] id of the handler
28+
def add_console_message_handler(&block)
29+
@log_handler.add_message_handler('console', &block)
30+
end
31+
32+
# @return [int] id of the handler
33+
def add_javascript_error_handler(&block)
34+
@log_handler.add_message_handler('javascript', &block)
35+
end
36+
37+
# @param [int] id of the handler previously added
38+
def remove_console_message_handler(id)
39+
@log_handler.remove_message_handler(id)
40+
end
41+
42+
alias remove_javascript_error_handler remove_console_message_handler
43+
end # Script
44+
end # WebDriver
45+
end # Selenium

rb/lib/selenium/webdriver/common/websocket_connection.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,18 @@ def callbacks
5252
@callbacks ||= Hash.new { |callbacks, event| callbacks[event] = [] }
5353
end
5454

55+
def add_callback(event, &block)
56+
callbacks[event] << block
57+
block.object_id
58+
end
59+
60+
def remove_callback(event, id)
61+
return if callbacks[event].reject! { |callback| callback.object_id == id }
62+
63+
ids = callbacks[event]&.map(&:object_id)
64+
raise Error::WebDriverError, "Callback with ID #{id} does not exist for event #{event}: #{ids}"
65+
end
66+
5567
def send_cmd(**payload)
5668
id = next_id
5769
data = payload.merge(id: id)

rb/sig/lib/selenium/webdriver/common/driver.rbs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module Selenium
88
@devtools: untyped
99
@navigate: untyped
1010

11+
@script: untyped
1112
@service_manager: untyped
1213

1314
def self.for: (untyped browser, Hash[untyped, untyped] opts) -> untyped

0 commit comments

Comments
 (0)