bidi2pdf 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -1
  3. data/CHANGELOG.md +63 -8
  4. data/README.md +28 -0
  5. data/docker/Dockerfile +1 -1
  6. data/docker/Dockerfile.chromedriver +9 -2
  7. data/docker/Dockerfile.slim +2 -2
  8. data/lib/bidi2pdf/bidi/browser_console_logger.rb +92 -0
  9. data/lib/bidi2pdf/bidi/browser_tab.rb +431 -41
  10. data/lib/bidi2pdf/bidi/client.rb +85 -23
  11. data/lib/bidi2pdf/bidi/command_manager.rb +46 -60
  12. data/lib/bidi2pdf/bidi/commands/base.rb +39 -1
  13. data/lib/bidi2pdf/bidi/commands/browser_remove_user_context.rb +27 -0
  14. data/lib/bidi2pdf/bidi/commands/browsing_context_print.rb +4 -0
  15. data/lib/bidi2pdf/bidi/commands/print_parameters_validator.rb +5 -0
  16. data/lib/bidi2pdf/bidi/commands.rb +1 -0
  17. data/lib/bidi2pdf/bidi/connection_manager.rb +3 -9
  18. data/lib/bidi2pdf/bidi/event_manager.rb +2 -2
  19. data/lib/bidi2pdf/bidi/interceptor.rb +1 -1
  20. data/lib/bidi2pdf/bidi/js_logger_helper.rb +16 -0
  21. data/lib/bidi2pdf/bidi/logger_events.rb +25 -45
  22. data/lib/bidi2pdf/bidi/navigation_failed_events.rb +41 -0
  23. data/lib/bidi2pdf/bidi/network_event.rb +15 -0
  24. data/lib/bidi2pdf/bidi/network_event_formatters/network_event_console_formatter.rb +4 -3
  25. data/lib/bidi2pdf/bidi/network_events.rb +27 -17
  26. data/lib/bidi2pdf/bidi/session.rb +123 -13
  27. data/lib/bidi2pdf/bidi/user_context.rb +62 -0
  28. data/lib/bidi2pdf/bidi/web_socket_dispatcher.rb +7 -7
  29. data/lib/bidi2pdf/chromedriver_manager.rb +48 -21
  30. data/lib/bidi2pdf/cli.rb +10 -2
  31. data/lib/bidi2pdf/dsl.rb +33 -0
  32. data/lib/bidi2pdf/launcher.rb +30 -0
  33. data/lib/bidi2pdf/notifications/event.rb +52 -0
  34. data/lib/bidi2pdf/notifications/instrumenter.rb +65 -0
  35. data/lib/bidi2pdf/notifications/logging_subscriber.rb +136 -0
  36. data/lib/bidi2pdf/notifications.rb +78 -0
  37. data/lib/bidi2pdf/session_runner.rb +35 -3
  38. data/lib/bidi2pdf/test_helpers/matchers/contains_pdf_text.rb +50 -0
  39. data/lib/bidi2pdf/test_helpers/matchers/have_pdf_page_count.rb +50 -0
  40. data/lib/bidi2pdf/test_helpers/matchers/match_pdf_text.rb +45 -0
  41. data/lib/bidi2pdf/test_helpers/pdf_reader_utils.rb +89 -0
  42. data/lib/bidi2pdf/test_helpers/pdf_text_sanitizer.rb +232 -0
  43. data/lib/bidi2pdf/test_helpers/testcontainers/chromedriver_container.rb +87 -0
  44. data/lib/bidi2pdf/test_helpers.rb +13 -0
  45. data/lib/bidi2pdf/verbose_logger.rb +79 -0
  46. data/lib/bidi2pdf/version.rb +1 -1
  47. data/lib/bidi2pdf.rb +131 -10
  48. data/sig/bidi2pdf/bidi/client.rbs +1 -1
  49. metadata +67 -4
  50. data/lib/bidi2pdf/utils.rb +0 -15
@@ -1,14 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "network_event"
4
+ require_relative "browser_console_logger"
4
5
 
5
6
  module Bidi2pdf
6
7
  module Bidi
7
8
  class LoggerEvents
8
- attr_reader :context_id
9
+ attr_reader :context_id, :browser_console_logger
9
10
 
10
11
  def initialize(context_id)
11
12
  @context_id = context_id
13
+ @browser_console_logger = BrowserConsoleLogger.new(Bidi2pdf.browser_console_logger)
12
14
  end
13
15
 
14
16
  def handle_event(data)
@@ -18,9 +20,11 @@ module Bidi2pdf
18
20
  if event.dig("source", "context") == context_id
19
21
  handle_response(method, event)
20
22
  else
21
- Bidi2pdf.logger.debug "Ignoring Log event: #{method}, context_id: #{context_id}, params: #{event}"
23
+ # this should be Bidi2pdf.logger and not Bidi2pdf.browser_console_logger
24
+ Bidi2pdf.logger.debug2 "Ignoring Log event: #{method}, context_id: #{context_id}, params: #{event}"
22
25
  end
23
26
  rescue StandardError => e
27
+ # this should be Bidi2pdf.logger and not Bidi2pdf.browser_console_logger
24
28
  Bidi2pdf.logger.error "Error handling Log event: #{e.message}\n#{e.backtrace&.join("\n")}"
25
29
  end
26
30
 
@@ -29,58 +33,34 @@ module Bidi2pdf
29
33
  text = event["text"]
30
34
  args = event["args"] || []
31
35
  stack_trace = event["stackTrace"]
32
- timestamp = format_timestamp(event["timestamp"])
33
- prefix = log_prefix(timestamp)
34
-
35
- log_message(level, prefix, text)
36
- log_args(prefix, args)
37
- log_stack_trace(prefix, stack_trace) if stack_trace && level == :error
38
- end
39
-
40
- private
41
-
42
- def log_message(level, prefix, text)
43
- return unless text
44
-
45
- Bidi2pdf.logger.send(level, "#{prefix} #{text}")
46
- end
47
-
48
- def log_args(prefix, args)
49
- return if args.empty?
50
-
51
- Bidi2pdf.logger.debug("#{prefix} Args: #{args.inspect}")
52
- end
53
-
54
- def log_stack_trace(prefix, trace)
55
- formatted_trace = format_stack_trace(trace)
56
- Bidi2pdf.logger.error("#{prefix} Stack trace captured:\n#{formatted_trace}")
57
- end
58
-
59
- def format_timestamp(timestamp)
60
- return "N/A" unless timestamp
61
-
62
- Time.at(timestamp.to_f / 1000).utc.strftime("%Y-%m-%d %H:%M:%S.%L UTC")
63
- end
64
-
65
- def format_stack_trace(trace)
66
- trace["callFrames"].each_with_index.map do |frame, index|
67
- function = frame["functionName"].to_s.empty? ? "(anonymous)" : frame["functionName"]
68
- "##{index} #{function} at #{frame["url"]}:#{frame["lineNumber"]}:#{frame["columnNumber"]}"
69
- end.join("\n")
36
+ timestamp = event["timestamp"]
37
+
38
+ Bidi2pdf.notification_service.instrument("browser_console_log_received.bidi2pdf",
39
+ {
40
+ level: level,
41
+ text: text,
42
+ args: args,
43
+ stack_trace: stack_trace,
44
+ timestamp: timestamp
45
+ })
46
+
47
+ browser_console_logger.builder
48
+ .with_level(level)
49
+ .with_timestamp(timestamp)
50
+ .with_text(text)
51
+ .with_args(args)
52
+ .with_stack_trace(stack_trace)
53
+ .log_event
70
54
  end
71
55
 
72
56
  def resolve_log_level(js_level)
73
57
  case js_level
74
- when "info", "warn", "error"
58
+ when "info", "warn", "error", "trace"
75
59
  js_level.to_sym
76
60
  else
77
61
  :debug
78
62
  end
79
63
  end
80
-
81
- def log_prefix(timestamp)
82
- "[#{timestamp}][Browser Console Log]"
83
- end
84
64
  end
85
65
  end
86
66
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "browser_console_logger"
4
+
5
+ module Bidi2pdf
6
+ module Bidi
7
+ class NavigationFailedEvents
8
+ attr_reader :context_id, :browser_console_logger
9
+
10
+ def initialize(context_id)
11
+ @context_id = context_id
12
+ end
13
+
14
+ def handle_event(data)
15
+ event = data["params"]
16
+ method = data["method"]
17
+
18
+ if event["context"] == context_id
19
+ handle_response(method, event)
20
+ else
21
+ Bidi2pdf.logger.debug2 "Ignoring Log event: #{method}, context_id: #{context_id}, params: #{event}"
22
+ end
23
+ end
24
+
25
+ def handle_response(_method, event)
26
+ url = event["url"]
27
+ navigation = event["navigation"]
28
+ timestamp = event["timestamp"]
29
+
30
+ Bidi2pdf.notification_service.instrument("navigation_failed_received.bidi2pdf",
31
+ {
32
+ url: url,
33
+ timestamp: timestamp,
34
+ navigation: navigation
35
+ })
36
+
37
+ Bidi2pdf.logger.error "Navigation failed for URL: #{url}, Navigation: #{navigation}"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -67,6 +67,21 @@ module Bidi2pdf
67
67
  "end=#{end_str}, " \
68
68
  "duration=#{took_str}>"
69
69
  end
70
+
71
+ def dup
72
+ self.class.new(
73
+ id: @id,
74
+ url: @url,
75
+ timestamp: @start_timestamp,
76
+ timing: @timing&.dup,
77
+ state: @state,
78
+ http_status_code: @http_status_code,
79
+ http_method: @http_method
80
+ ).tap do |duped|
81
+ duped.instance_variable_set(:@end_timestamp, @end_timestamp)
82
+ duped.instance_variable_set(:@bytes_received, @bytes_received)
83
+ end
84
+ end
70
85
  end
71
86
  end
72
87
  end
@@ -7,7 +7,7 @@ module Bidi2pdf
7
7
  class NetworkEventConsoleFormatter
8
8
  include NetworkEventFormatterUtils
9
9
 
10
- attr_reader :color_enabled
10
+ attr_reader :color_enabled, :logger
11
11
 
12
12
  # ANSI styles
13
13
  RESET = "\e[0m"
@@ -19,12 +19,13 @@ module Bidi2pdf
19
19
  CYAN = "\e[36m"
20
20
  GRAY = "\e[90m"
21
21
 
22
- def initialize(color: true)
22
+ def initialize(color: true, logger: Bidi2pdf.network_events_logger)
23
23
  @color_enabled = color
24
+ @logger = logger
24
25
  end
25
26
 
26
27
  def log(events)
27
- events.each { |event| pretty_log(event).each_line { |line| Bidi2pdf.network_events_logger.info(line.chomp) } }
28
+ events.each { |event| pretty_log(event).each_line { |line| logger.info(line.chomp) } }
28
29
  end
29
30
 
30
31
  def pretty_log(event)
@@ -21,13 +21,13 @@ module Bidi2pdf
21
21
  if event["context"] == context_id
22
22
  handle_response(method, event)
23
23
  else
24
- Bidi2pdf.logger.debug "Ignoring Network event: #{method}, #{context_id}, params: #{event}"
24
+ Bidi2pdf.logger.debug3 "Ignoring Network event: #{method}, #{context_id}, params: #{event}"
25
25
  end
26
26
  rescue StandardError => e
27
27
  Bidi2pdf.logger.error "Error handling network event: #{e.message}"
28
28
  end
29
29
 
30
- # rubocop:disable Metrics/AbcSize
30
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
31
31
  def handle_response(method, event)
32
32
  return unless event && event["request"]
33
33
 
@@ -43,23 +43,33 @@ module Bidi2pdf
43
43
 
44
44
  timestamp = event["timestamp"]
45
45
 
46
- if method == "network.beforeRequestSent"
47
- events[id] ||= NetworkEvent.new(
48
- id: id,
49
- url: url,
50
- timestamp: timestamp,
51
- timing: timing,
52
- state: method,
53
- http_method: http_method
54
- )
55
- elsif events.key?(id)
56
- events[id].update_state(method, timestamp: timestamp, timing: timing, http_status_code: http_status_code, bytes_received: bytes_received)
57
- else
58
- Bidi2pdf.logger.warn "Received response for unknown request ID: #{id}, URL: #{url}"
46
+ Bidi2pdf.notification_service.instrument("network_event_received.bidi2pdf",
47
+ {
48
+ id: id,
49
+ method: method,
50
+ url: url,
51
+ http_status_code: http_status_code
52
+ }) do |instrumentation_payload|
53
+ if method == "network.beforeRequestSent"
54
+ events[id] ||= NetworkEvent.new(
55
+ id: id,
56
+ url: url,
57
+ timestamp: timestamp,
58
+ timing: timing,
59
+ state: method,
60
+ http_method: http_method
61
+ )
62
+ elsif events.key?(id)
63
+ events[id].update_state(method, timestamp: timestamp, timing: timing, http_status_code: http_status_code, bytes_received: bytes_received)
64
+ else
65
+ Bidi2pdf.logger.warn "Received response for unknown request ID: #{id}, URL: #{url}"
66
+ end
67
+
68
+ instrumentation_payload[:event] = events[id]&.dup
59
69
  end
60
70
  end
61
71
 
62
- # rubocop:enable Metrics/AbcSize
72
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
63
73
 
64
74
  def all_events
65
75
  events.values.sort_by(&:start_timestamp)
@@ -77,7 +87,7 @@ module Bidi2pdf
77
87
  end
78
88
  end
79
89
 
80
- def wait_until_network_idle(timeout: 10, poll_interval: 0.1)
90
+ def wait_until_network_idle(timeout: 10, poll_interval: 0.01)
81
91
  start_time = Time.now
82
92
 
83
93
  loop do
@@ -7,14 +7,47 @@ require_relative "client"
7
7
  require_relative "browser"
8
8
  require_relative "user_context"
9
9
 
10
+ # Represents a session for managing browser interactions and communication
11
+ # using the Bidi2pdf library. This class handles the setup, configuration,
12
+ # and execution of browser-related workflows, including session creation,
13
+ # WebSocket communication, and browser management.
14
+ #
15
+ # @example Creating and starting a session
16
+ # session = Bidi2pdf::Bidi::Session.new(session_url: "http://example.com/session", headless: true)
17
+ # session.start
18
+ #
19
+ # @example Retrieving user contexts
20
+ # session.user_contexts
21
+ #
22
+ # @example Closing the session
23
+ # session.close
24
+ #
25
+ # @param [String] session_url The URL for the session.
26
+ # @param [Boolean] headless Whether to run the browser in headless mode. Defaults to true.
27
+ # @param [Array<String>] chrome_args Additional Chrome arguments. Defaults to predefined arguments.
10
28
  module Bidi2pdf
11
29
  module Bidi
12
30
  class Session
31
+ # Events to subscribe to during the session.
13
32
  SUBSCRIBE_EVENTS = %w[script].freeze
33
+
34
+ # Default Chrome arguments for the session.
14
35
  DEFAULT_CHROME_ARGS = %w[--disable-gpu --disable-popup-blocking --disable-hang-monitor].freeze
15
36
 
16
- attr_reader :session_uri, :started, :chrome_args
37
+ # @return [URI] The URI of the session.
38
+ attr_reader :session_uri
39
+
40
+ # @return [Boolean] Whether the session has started.
41
+ attr_reader :started
42
+
43
+ # @return [Array<String>] The Chrome arguments for the session.
44
+ attr_reader :chrome_args
17
45
 
46
+ # Initializes a new session.
47
+ #
48
+ # @param [String] session_url The URL for the session.
49
+ # @param [Boolean] headless Whether to run the browser in headless mode. Defaults to true.
50
+ # @param [Array<String>] chrome_args Additional Chrome arguments. Defaults to predefined arguments.
18
51
  def initialize(session_url:, headless: true, chrome_args: DEFAULT_CHROME_ARGS)
19
52
  @session_uri = URI(session_url)
20
53
  @headless = headless
@@ -22,6 +55,9 @@ module Bidi2pdf
22
55
  @chrome_args = chrome_args.dup
23
56
  end
24
57
 
58
+ # Starts the session and initializes the client.
59
+ #
60
+ # @raise [StandardError] If an error occurs during session start.
25
61
  def start
26
62
  return if started?
27
63
 
@@ -32,41 +68,71 @@ module Bidi2pdf
32
68
  raise e
33
69
  end
34
70
 
71
+ # Returns the WebSocket client for the session.
72
+ #
73
+ # @return [Bidi2pdf::Bidi::Client, nil] The WebSocket client, or nil if the session is not started.
35
74
  def client
36
75
  @client ||= started? ? create_client : nil
37
76
  end
38
77
 
78
+ # Returns the browser instance for the session.
79
+ #
80
+ # @return [Bidi2pdf::Bidi::Browser] The browser instance.
39
81
  def browser
40
82
  @browser ||= create_browser
41
83
  end
42
84
 
85
+ # Closes the session and cleans up resources.
86
+ # rubocop:disable Metrics/AbcSize
43
87
  def close
44
88
  return unless started?
45
89
 
46
90
  2.times do |attempt|
47
- client&.send_cmd_and_wait(Bidi2pdf::Bidi::Commands::SessionEnd.new, timeout: 1) do |response|
48
- Bidi2pdf.logger.info "Session ended: #{response}"
49
-
50
- cleanup
91
+ success = Bidi2pdf.notification_service.instrument("session_close.bidi2pdf", { session_uri: session_uri.to_s, attempt: attempt + 1 }) do |payload|
92
+ client&.send_cmd_and_wait(Bidi2pdf::Bidi::Commands::SessionEnd.new, timeout: 1) do |response|
93
+ payload[:response] = response
94
+ cleanup
95
+ end
96
+
97
+ true
98
+ rescue CmdTimeoutError => e
99
+ payload[:error] = e
100
+ payload[:retry] = attempt < 1 # whether we'll retry again
101
+
102
+ false
51
103
  end
52
- break
53
- rescue CmdTimeoutError
54
- Bidi2pdf.logger.error "Session end command timed out. Retrying... (#{attempt + 1})"
104
+
105
+ break if success
55
106
  end
107
+ ensure
108
+ @started = false
56
109
  end
57
110
 
111
+ # rubocop: enable Metrics/AbcSize
112
+
113
+ # Retrieves user contexts for the session.
58
114
  def user_contexts
59
115
  send_cmd(Bidi2pdf::Bidi::Commands::GetUserContexts.new) { |resp| Bidi2pdf.logger.debug "User contexts: #{resp}" }
60
116
  end
61
117
 
118
+ # Retrieves the status of the session.
62
119
  def status
63
- send_cmd(Bidi2pdf::Bidi::Commands::SessionStatus.new) { |resp| Bidi2pdf.logger.info "Session status: #{resp.inspect}" }
120
+ send_cmd(Bidi2pdf::Bidi::Commands::SessionStatus.new) do |resp|
121
+ Bidi2pdf.logger.info "Session status: #{resp["result"].inspect}"
122
+ resp["result"]
123
+ end
64
124
  end
65
125
 
126
+ # Checks if the session has started.
127
+ #
128
+ # @return [Boolean] True if the session has started, false otherwise.
66
129
  def started?
67
130
  @started
68
131
  end
69
132
 
133
+ # Retrieves the WebSocket URL for the session.
134
+ #
135
+ # @return [String] The WebSocket URL.
70
136
  def websocket_url
71
137
  return @websocket_url if @websocket_url
72
138
 
@@ -79,11 +145,18 @@ module Bidi2pdf
79
145
 
80
146
  private
81
147
 
82
- def send_cmd(command, &block)
83
- client&.send_cmd_and_wait(command, &block)
148
+ # Sends a command to the WebSocket client.
149
+ #
150
+ # @param [Object] command The command to send.
151
+ # @yield [response] A block to handle the response.
152
+ def send_cmd(command, &)
153
+ client&.send_cmd_and_wait(command, &)
84
154
  end
85
155
 
86
- # rubocop: disable Metrics/AbcSize
156
+ # Creates a new browser instance.
157
+ #
158
+ # @return [Bidi2pdf::Bidi::Browser] The browser instance.
159
+ # rubocop:disable Metrics/AbcSize
87
160
  def create_browser
88
161
  start
89
162
  client.start
@@ -103,12 +176,18 @@ module Bidi2pdf
103
176
  Bidi::Browser.new(client)
104
177
  end
105
178
 
106
- # rubocop: enable Metrics/AbcSize
179
+ # rubocop:enable Metrics/AbcSize
107
180
 
181
+ # Creates a new WebSocket client.
182
+ #
183
+ # @return [Bidi2pdf::Bidi::Client] The WebSocket client.
108
184
  def create_client
109
185
  Bidi::Client.new(websocket_url).tap(&:start)
110
186
  end
111
187
 
188
+ # Creates a new session and retrieves the WebSocket URL.
189
+ #
190
+ # @return [String] The WebSocket URL.
112
191
  def create_new_session
113
192
  session_data = exec_api_call(session_request)
114
193
  Bidi2pdf.logger.debug "Session data: #{session_data}"
@@ -124,6 +203,9 @@ module Bidi2pdf
124
203
  ws_url
125
204
  end
126
205
 
206
+ # Builds the session request payload.
207
+ #
208
+ # @return [Hash] The session request payload.
127
209
  def session_request
128
210
  session_chrome_args = chrome_args.dup
129
211
  session_chrome_args << "--headless" if @headless
@@ -142,6 +224,10 @@ module Bidi2pdf
142
224
  }
143
225
  end
144
226
 
227
+ # Executes an API call with the given payload.
228
+ #
229
+ # @param [Hash] payload The payload for the API call.
230
+ # @return [Hash] The parsed response data.
145
231
  def exec_api_call(payload)
146
232
  response = Net::HTTP.post(session_uri, payload.to_json, "Content-Type" => "application/json")
147
233
  body = response.body
@@ -158,11 +244,20 @@ module Bidi2pdf
158
244
  build_error(error_type, "#{error_description(error_type)} #{e.message}", e.backtrace)
159
245
  end
160
246
 
247
+ # Logs an API error.
248
+ #
249
+ # @param [String] message The error message.
250
+ # @param [Integer] code The response code.
251
+ # @param [String] body The response body.
161
252
  def log_api_error(message, code, body)
162
253
  Bidi2pdf.logger.error "#{message}. Response code: #{code}"
163
254
  Bidi2pdf.logger.error "Response body: #{body}"
164
255
  end
165
256
 
257
+ # Determines the error category based on the exception.
258
+ #
259
+ # @param [Exception] exception The exception to categorize.
260
+ # @return [String] The error category.
166
261
  def error_category(exception)
167
262
  case exception
168
263
  when Errno::ECONNREFUSED then "Connection refused"
@@ -171,6 +266,10 @@ module Bidi2pdf
171
266
  end
172
267
  end
173
268
 
269
+ # Retrieves the error description for a given error type.
270
+ #
271
+ # @param [String] type The error type.
272
+ # @return [String] The error description.
174
273
  def error_description(type)
175
274
  {
176
275
  "Connection refused" => "Could not connect to the session URL:",
@@ -179,6 +278,12 @@ module Bidi2pdf
179
278
  }[type]
180
279
  end
181
280
 
281
+ # Builds an error response.
282
+ #
283
+ # @param [String] error The error type.
284
+ # @param [String] message The error message.
285
+ # @param [Array<String>, nil] backtrace The error backtrace.
286
+ # @return [Hash] The error response.
182
287
  def build_error(error, message, backtrace = nil)
183
288
  {
184
289
  "value" => {
@@ -189,6 +294,10 @@ module Bidi2pdf
189
294
  }
190
295
  end
191
296
 
297
+ # Handles an error response from the session.
298
+ #
299
+ # @param [Hash] value The error response value.
300
+ # @raise [SessionNotStartedError] If the session could not be started.
192
301
  def handle_error(value)
193
302
  error = value["error"]
194
303
  return unless error
@@ -208,6 +317,7 @@ module Bidi2pdf
208
317
  "Session not started. Check logs for more details. Error: #{error} message: #{msg}"
209
318
  end
210
319
 
320
+ # Cleans up resources associated with the session.
211
321
  def cleanup
212
322
  @client&.close
213
323
  @client = @websocket_url = @browser = nil
@@ -4,14 +4,41 @@ require_relative "browser_tab"
4
4
 
5
5
  module Bidi2pdf
6
6
  module Bidi
7
+ # Represents a user context for managing browser interactions and cookies
8
+ # using the Bidi2pdf library. This class provides methods for creating
9
+ # user contexts, setting cookies, and creating browser windows.
10
+ #
11
+ # @example Creating a user context
12
+ # user_context = Bidi2pdf::Bidi::UserContext.new(client)
13
+ #
14
+ # @example Setting a cookie
15
+ # user_context.set_cookie(
16
+ # name: "session",
17
+ # value: "abc123",
18
+ # domain: "example.com",
19
+ # source_origin: "http://example.com"
20
+ # )
21
+ #
22
+ # @example Creating a browser window
23
+ # browser_window = user_context.create_browser_window
24
+ #
25
+ # @param [Object] client The WebSocket client for communication.
7
26
  class UserContext
27
+ # @return [Object] The WebSocket client.
8
28
  attr_reader :client
9
29
 
30
+ # Initializes a new user context.
31
+ #
32
+ # @param [Object] client The WebSocket client for communication.
10
33
  def initialize(client)
11
34
  @client = client
12
35
  @context_id = nil
13
36
  end
14
37
 
38
+ # Retrieves the user context ID, creating it if it does not exist.
39
+ #
40
+ # @return [String] The user context ID.
41
+ # @raise [RuntimeError] If an error occurs while creating the user context.
15
42
  def context_id
16
43
  @context_id ||= begin
17
44
  res = client.send_cmd_and_wait(Bidi2pdf::Bidi::Commands::BrowserCreateUserContext.new) do |response|
@@ -26,6 +53,17 @@ module Bidi2pdf
26
53
  end
27
54
  end
28
55
 
56
+ # Sets a cookie in the user context.
57
+ #
58
+ # @param [String] name The name of the cookie.
59
+ # @param [String] value The value of the cookie.
60
+ # @param [String] domain The domain for the cookie.
61
+ # @param [String] source_origin The source origin for the cookie.
62
+ # @param [String] path The path for the cookie. Defaults to "/".
63
+ # @param [Boolean] secure Whether the cookie is secure. Defaults to true.
64
+ # @param [Boolean] http_only Whether the cookie is HTTP-only. Defaults to false.
65
+ # @param [String] same_site The SameSite attribute for the cookie. Defaults to "strict".
66
+ # @param [Integer] ttl The time-to-live for the cookie in seconds. Defaults to 30.
29
67
  def set_cookie(
30
68
  name:,
31
69
  value:,
@@ -55,6 +93,9 @@ module Bidi2pdf
55
93
  end
56
94
  end
57
95
 
96
+ # Creates a new browser window in the user context.
97
+ #
98
+ # @return [BrowserTab] The newly created browser tab.
58
99
  def create_browser_window
59
100
  cmd = Bidi2pdf::Bidi::Commands::CreateWindow.new(user_context_id: context_id)
60
101
 
@@ -64,6 +105,27 @@ module Bidi2pdf
64
105
  BrowserTab.new(client, browsing_context_id, context_id)
65
106
  end
66
107
  end
108
+
109
+ # Closes the user context.
110
+ #
111
+ # This method removes the user context from the browser, effectively cleaning up
112
+ # any associated resources. If the user context does not exist, the method does nothing.
113
+ #
114
+ # @return [nil]
115
+ # @raise [RuntimeError] If an error occurs while removing the user context.
116
+ def close
117
+ return unless context_id
118
+
119
+ res = client.send_cmd_and_wait(Bidi2pdf::Bidi::Commands::BrowserRemoveUserContext.new(user_context_id: context_id)) do |response|
120
+ raise "Error removing user context: #{response.inspect}" if response["error"]
121
+
122
+ response["result"]
123
+ end
124
+
125
+ Bidi2pdf.logger.debug "User context deleted: #{res.inspect}"
126
+
127
+ @context_id = nil
128
+ end
67
129
  end
68
130
  end
69
131
  end
@@ -22,15 +22,15 @@ module Bidi2pdf
22
22
 
23
23
  # Add listeners
24
24
 
25
- def on_message(&block) = socket_events.on(:message, &block)
25
+ def on_message(&) = socket_events.on(:message, &)
26
26
 
27
- def on_event(name, &block) = session_events.on(name, &block)
27
+ def on_event(name, &) = session_events.on(name, &)
28
28
 
29
- def on_open(&block) = socket_events.on(:open, &block)
29
+ def on_open(&) = socket_events.on(:open, &)
30
30
 
31
- def on_close(&block) = socket_events.on(:close, &block)
31
+ def on_close(&) = socket_events.on(:close, &)
32
32
 
33
- def on_error(&block) = socket_events.on(:error, &block)
33
+ def on_error(&) = socket_events.on(:error, &)
34
34
 
35
35
  def remove_message_listener(block) = socket_events.off(:message, block)
36
36
 
@@ -52,10 +52,10 @@ module Bidi2pdf
52
52
  method = data["method"]
53
53
 
54
54
  if method
55
- Bidi2pdf.logger.debug "Dispatching session event: #{method}"
55
+ Bidi2pdf.logger.debug3 "Dispatching session event: #{method}"
56
56
  that.session_events.dispatch(method, data)
57
57
  else
58
- Bidi2pdf.logger.debug "Dispatching socket message"
58
+ Bidi2pdf.logger.debug3 "Dispatching socket message"
59
59
  that.socket_events.dispatch(:message, data)
60
60
  end
61
61
  end