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
@@ -4,14 +4,72 @@ require "base64"
4
4
 
5
5
  require_relative "network_events"
6
6
  require_relative "logger_events"
7
+ require_relative "navigation_failed_events"
7
8
  require_relative "auth_interceptor"
8
9
  require_relative "add_headers_interceptor"
9
-
10
+ require_relative "js_logger_helper"
11
+
12
+ # Represents a browser tab for managing interactions and communication
13
+ # using the Bidi2pdf library. This class provides methods for creating
14
+ # browser tabs, managing cookies, navigating to URLs, executing scripts,
15
+ # and handling network events.
16
+ #
17
+ # @example Creating a browser tab
18
+ # browser_tab = Bidi2pdf::Bidi::BrowserTab.new(client, browsing_context_id, user_context_id)
19
+ # browser_tab.create_browser_tab
20
+ #
21
+ # @example Navigating to a URL
22
+ # browser_tab.navigate_to("http://example.com")
23
+ #
24
+ # @example Setting a cookie
25
+ # browser_tab.set_cookie(
26
+ # name: "session",
27
+ # value: "abc123",
28
+ # domain: "example.com"
29
+ # )
30
+ #
31
+ # @param [Object] client The WebSocket client for communication.
32
+ # @param [String] browsing_context_id The ID of the browsing context.
33
+ # @param [String] user_context_id The ID of the user context.
10
34
  module Bidi2pdf
11
35
  module Bidi
36
+ # Represents a browser tab for managing interactions and communication
37
+ # using the Bidi2pdf library. This class provides methods for creating
38
+ # browser tabs, managing cookies, navigating to URLs, executing scripts,
39
+ # handling network events, and general tab lifecycle management.
40
+ #
12
41
  class BrowserTab
13
- attr_reader :client, :browsing_context_id, :user_context_id, :tabs, :network_events, :open, :logger_events
42
+ include JsLoggerHelper
43
+
44
+ # @return [Object] The WebSocket client.
45
+ attr_reader :client
46
+
47
+ # @return [String] The browsing context ID.
48
+ attr_reader :browsing_context_id
49
+
50
+ # @return [String] The user context ID.
51
+ attr_reader :user_context_id
52
+
53
+ # @return [Array<BrowserTab>] The list of tabs.
54
+ attr_reader :tabs
14
55
 
56
+ # @return [NetworkEvents] The network events handler.
57
+ attr_reader :network_events
58
+
59
+ # @return [Boolean] Whether the tab is open.
60
+ attr_reader :open
61
+
62
+ # @return [LoggerEvents] The logger events handler.
63
+ attr_reader :logger_events
64
+
65
+ # @return [NavigationFailedEvents] The navigation failed events handler.
66
+ attr_reader :navigation_failed_events
67
+
68
+ # Initializes a new browser tab.
69
+ #
70
+ # @param [Object] client The WebSocket client for communication.
71
+ # @param [String] browsing_context_id The ID of the browsing context.
72
+ # @param [String] user_context_id The ID of the user context.
15
73
  def initialize(client, browsing_context_id, user_context_id)
16
74
  @client = client
17
75
  @browsing_context_id = browsing_context_id
@@ -19,9 +77,13 @@ module Bidi2pdf
19
77
  @tabs = []
20
78
  @network_events = NetworkEvents.new browsing_context_id
21
79
  @logger_events = LoggerEvents.new browsing_context_id
80
+ @navigation_failed_events = NavigationFailedEvents.new browsing_context_id
22
81
  @open = true
23
82
  end
24
83
 
84
+ # Creates a new browser tab.
85
+ #
86
+ # @return [BrowserTab] The newly created browser tab.
25
87
  def create_browser_tab
26
88
  cmd = Bidi2pdf::Bidi::Commands::CreateTab.new(user_context_id: user_context_id)
27
89
  client.send_cmd_and_wait(cmd) do |response|
@@ -29,11 +91,21 @@ module Bidi2pdf
29
91
 
30
92
  BrowserTab.new(client, tab_browsing_context_id, user_context_id).tap do |tab|
31
93
  tabs << tab
32
- Bidi2pdf.logger.debug "Created new browser tab: #{tab.inspect}"
94
+ Bidi2pdf.logger.debug1 "Created new browser tab: #{tab.inspect}"
33
95
  end
34
96
  end
35
97
  end
36
98
 
99
+ # Sets a cookie in the browser tab.
100
+ #
101
+ # @param [String] name The name of the cookie.
102
+ # @param [String] value The value of the cookie.
103
+ # @param [String] domain The domain for the cookie.
104
+ # @param [String] path The path for the cookie. Defaults to "/".
105
+ # @param [Boolean] secure Whether the cookie is secure. Defaults to true.
106
+ # @param [Boolean] http_only Whether the cookie is HTTP-only. Defaults to false.
107
+ # @param [String] same_site The SameSite attribute for the cookie. Defaults to "strict".
108
+ # @param [Integer] ttl The time-to-live for the cookie in seconds. Defaults to 30.
37
109
  def set_cookie(
38
110
  name:,
39
111
  value:,
@@ -56,10 +128,15 @@ module Bidi2pdf
56
128
  ttl: ttl
57
129
  )
58
130
  client.send_cmd_and_wait(cmd) do |response|
59
- Bidi2pdf.logger.debug "Cookie set: #{response.inspect}"
131
+ Bidi2pdf.logger.debug1 "Cookie set: #{response.inspect}"
60
132
  end
61
133
  end
62
134
 
135
+ # Adds headers to requests in the browser tab.
136
+ #
137
+ # @param [Hash] headers The headers to add.
138
+ # @param [Array<String>] url_patterns The URL patterns to match.
139
+ # @return [AddHeadersInterceptor] The interceptor instance.
63
140
  def add_headers(
64
141
  headers:,
65
142
  url_patterns:
@@ -71,6 +148,12 @@ module Bidi2pdf
71
148
  ).tap { |interceptor| interceptor.register_with_client(client: client) }
72
149
  end
73
150
 
151
+ # Configures basic authentication for requests in the browser tab.
152
+ #
153
+ # @param [String] username The username for authentication.
154
+ # @param [String] password The password for authentication.
155
+ # @param [Array<String>] url_patterns The URL patterns to match.
156
+ # @return [AuthInterceptor] The interceptor instance.
74
157
  def basic_auth(username:, password:, url_patterns:)
75
158
  AuthInterceptor.new(
76
159
  context: browsing_context_id,
@@ -79,41 +162,175 @@ module Bidi2pdf
79
162
  ).tap { |interceptor| interceptor.register_with_client(client: client) }
80
163
  end
81
164
 
165
+ # Navigates the browser tab to a specified URL.
166
+ #
167
+ # This method registers necessary event listeners and sends a navigation
168
+ # command to the browser tab, instructing it to load the specified URL.
169
+ # It validates that the URL is properly formatted before attempting navigation.
170
+ #
171
+ # @param [String] url The URL to navigate to.
172
+ # @raise [NavigationError] If the URL is invalid or improperly formatted.
173
+ # @example
174
+ # browser_tab.navigate_to("https://example.com")
82
175
  def navigate_to(url)
83
- client.on_event("network.beforeRequestSent", "network.responseStarted", "network.responseCompleted", "network.fetchError",
84
- &network_events.method(:handle_event))
176
+ begin
177
+ URI.parse(url)
178
+ rescue URI::InvalidURIError => e
179
+ raise NavigationError, "Invalid URL: #{url} - #{e.message}"
180
+ end
85
181
 
86
- client.on_event("log.entryAdded",
87
- &logger_events.method(:handle_event))
182
+ Bidi2pdf.notification_service.instrument("navigate_to.bidi2pdf", url: url) do
183
+ navigate_with_listeners url
184
+ end
185
+ end
88
186
 
89
- cmd = Bidi2pdf::Bidi::Commands::BrowsingContextNavigate.new url: url, context: browsing_context_id
187
+ # Renders HTML content in the browser tab.
188
+ #
189
+ # @param [String] html_content The HTML content to render.
190
+ def render_html_content(html_content)
191
+ Bidi2pdf.notification_service.instrument("render_html_content.bidi2pdf", url: "data:text/html") do |instrumentation_payload|
192
+ base64_encoded = Base64.strict_encode64(html_content)
90
193
 
91
- client.send_cmd_and_wait(cmd) do |response|
92
- Bidi2pdf.logger.debug "Navigated to page url: #{url} response: #{response}"
194
+ instrumentation_payload[:data] = base64_encoded
195
+
196
+ data_url = "data:text/html;charset=utf-8;base64,#{base64_encoded}"
197
+
198
+ navigate_with_listeners data_url
93
199
  end
94
200
  end
95
201
 
96
- def render_html_content(html_content)
97
- base64_encoded = Base64.strict_encode64(html_content)
98
- data_url = "data:text/html;charset=utf-8;base64,#{base64_encoded}"
202
+ # Executes a script in the browser tab.
203
+ #
204
+ # This method allows you to execute JavaScript code within the context of the browser tab.
205
+ # Optionally, the script can be wrapped in a JavaScript Promise to handle asynchronous operations.
206
+ #
207
+ # @param [String] script The JavaScript code to execute.
208
+ # - This can be any valid JavaScript code that you want to run in the browser tab.
209
+ # @param [Boolean] wrap_in_promise Whether to wrap the script in a Promise. Defaults to false.
210
+ # - If true, the script will be wrapped in a Promise to handle asynchronous execution.
211
+ # - Use this option when the script involves asynchronous operations like network requests.
212
+ # You can use the predefined variable result to store the result of the script.
213
+ # @return [Object] The result of the script execution.
214
+ # - If the script executes successfully, the result of the last evaluated expression is returned.
215
+ # - If the script fails, an error or exception details may be returned.
216
+ def execute_script(script, wrap_in_promise: false)
217
+ Bidi2pdf.notification_service.instrument("execute_script.bidi2pdf") do
218
+ if wrap_in_promise
219
+ script = <<~JS
220
+ new Promise((resolve, reject) => {
221
+ try {
222
+ let result;
223
+
224
+ #{script}
225
+
226
+ resolve(result);
227
+ } catch (error) {
228
+ reject(error);
229
+ }
230
+ });
231
+ JS
232
+ end
99
233
 
100
- navigate_to(data_url)
234
+ cmd = Bidi2pdf::Bidi::Commands::ScriptEvaluate.new context: browsing_context_id, expression: script
235
+ client.send_cmd_and_wait(cmd) do |response|
236
+ Bidi2pdf.logger.debug2 "Script Result: #{response.inspect}"
237
+
238
+ response["result"]
239
+ end
240
+ end
101
241
  end
102
242
 
103
- def execute_script(script)
104
- cmd = Bidi2pdf::Bidi::Commands::ScriptEvaluate.new context: browsing_context_id, expression: script
105
- client.send_cmd_and_wait(cmd) do |response|
106
- Bidi2pdf.logger.debug "Script Result: #{response.inspect}"
243
+ # Injects a JavaScript script element into the page, either from a URL or with inline content.
244
+ #
245
+ # @param [String, nil] url The URL of the script to load (optional).
246
+ # @param [String, nil] content The JavaScript content to inject (optional).
247
+ # @param [String, nil] id The ID attribute for the script element (optional).
248
+ # @return [Object] The result from the script creation promise.
249
+ def inject_script(url: nil, content: nil, id: nil)
250
+ script_code = generate_script_element_code(url: url, content: content, id: id)
251
+ response = execute_script(script_code)
252
+
253
+ if response
254
+ if response["type"] == "exception"
255
+ handle_injection_exception(response, url, ScriptInjectionError)
256
+ elsif response["type"] == "success"
257
+ Bidi2pdf.logger.debug1 "Script injected successfully: #{response.inspect}"
258
+ response
259
+ else
260
+ Bidi2pdf.logger.warn "Script injected unknown state: #{response.inspect}"
261
+ response
262
+ end
263
+ else
264
+ Bidi2pdf.logger.error "Failed to inject script: #{url || content}"
265
+ raise ScriptInjectionError, "Failed to inject script: #{url || content}"
266
+ end
267
+ end
107
268
 
108
- response["result"]
269
+ # Injects a CSS style element into the page, either from a URL or with inline content.
270
+ #
271
+ # @param [String, nil] url The URL of the stylesheet to load (optional).
272
+ # @param [String, nil] content The CSS content to inject (optional).
273
+ # @param [String, nil] id The ID attribute for the style element (optional).
274
+ # @return [Object] The result from the style creation promise.
275
+ def inject_style(url: nil, content: nil, id: nil)
276
+ style_code = generate_style_element_code(url: url, content: content, id: id)
277
+ response = execute_script(style_code)
278
+
279
+ if response
280
+ if response["type"] == "exception"
281
+ handle_injection_exception(response, url, StyleInjectionError)
282
+ elsif response["type"] == "success"
283
+ Bidi2pdf.logger.debug1 "Style injected successfully: #{response.inspect}"
284
+ response
285
+ else
286
+ Bidi2pdf.logger.warn "Style injection unknown state: #{response.inspect}"
287
+ response
288
+ end
289
+ else
290
+ Bidi2pdf.logger.error "Failed to inject style: #{url || content}"
291
+ raise StyleInjectionError, "Failed to inject style: #{url || content}"
109
292
  end
110
293
  end
111
294
 
295
+ # Waits until the network is idle in the browser tab.
296
+ #
297
+ # @param [Integer] timeout The timeout duration in seconds. Defaults to 10.
298
+ # @param [Float] poll_interval The polling interval in seconds. Defaults to 0.1.
112
299
  def wait_until_network_idle(timeout: 10, poll_interval: 0.1)
113
- network_events.wait_until_network_idle(timeout: timeout, poll_interval: poll_interval)
300
+ Bidi2pdf.notification_service.instrument("network_idle.bidi2pdf") do |instrumentation_payload|
301
+ network_events.wait_until_network_idle(timeout: timeout, poll_interval: poll_interval)
302
+
303
+ instrumentation_payload[:requests] = network_events.all_events.dup
304
+ end
305
+ end
306
+
307
+ # Waits until the page is fully loaded in the browser tab.
308
+ #
309
+ # This method executes a JavaScript script that checks if the page
310
+ # has finished loading.
311
+ #
312
+ # @param [String] check_script The JavaScript code to check if the page is loaded.
313
+ # - Defaults to a script that polls the `window.loaded` property.
314
+ # @return [Object] The result of the script execution.
315
+ # - If the page is loaded successfully, the Promise resolves with the value `'done'`.
316
+ # - If the script fails, an error or exception details may be returned.
317
+ def wait_until_page_loaded(check_script: nil)
318
+ check_script ||= <<~JS
319
+ new Promise(resolve => { const check = () => window.loaded ? resolve('done') : setTimeout(check, 100); check(); });
320
+ JS
321
+
322
+ Bidi2pdf.notification_service.instrument("page_loaded.bidi2pdf") do
323
+ execute_script check_script
324
+ end
114
325
  end
115
326
 
116
- def log_network_traffic(format: :console, output: nil, print_options: { background: true }, &block)
327
+ # Logs network traffic in the browser tab.
328
+ #
329
+ # @param [Symbol] format The format for logging (:console or :pdf). Defaults to :console.
330
+ # @param [String, nil] output The output file for PDF logging. Defaults to nil.
331
+ # @param [Hash] print_options Options for printing. Defaults to { background: true }.
332
+ # @yield [pdf_base64] A block to handle the PDF content.
333
+ def log_network_traffic(format: :console, output: nil, print_options: { background: true }, &)
117
334
  format = format.to_sym
118
335
 
119
336
  if format == :console
@@ -128,12 +345,13 @@ module Bidi2pdf
128
345
  logging_tab.render_html_content(html_content)
129
346
  logging_tab.wait_until_network_idle
130
347
 
131
- logging_tab.print(output, print_options: print_options, &block)
348
+ logging_tab.print(output, print_options: print_options, &)
132
349
 
133
350
  logging_tab.close
134
351
  end
135
352
  end
136
353
 
354
+ # Closes the browser tab and its associated resources.
137
355
  def close
138
356
  return unless open
139
357
 
@@ -144,36 +362,203 @@ module Bidi2pdf
144
362
  @open = false
145
363
  end
146
364
 
147
- # rubocop: disable Metrics/AbcSize, Metrics/PerceivedComplexity
365
+ # Prints the content of the browser tab.
366
+ #
367
+ # @param [String, nil] outputfile The output file for the PDF. Defaults to nil.
368
+ # @param [Hash] print_options Options for printing. Defaults to { background: true }.
369
+ # @yield [pdf_base64] A block to handle the PDF content.
370
+ # @return [String, nil] The base64-encoded PDF content, or nil if outputfile or block is provided.
371
+ # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
148
372
  def print(outputfile = nil, print_options: { background: true }, &block)
149
- cmd = Bidi2pdf::Bidi::Commands::BrowsingContextPrint.new context: browsing_context_id, print_options: print_options
373
+ Bidi2pdf.notification_service.instrument("print.bidi2pdf") do |instrumentation_payload|
374
+ cmd = Bidi2pdf::Bidi::Commands::BrowsingContextPrint.new context: browsing_context_id, print_options: print_options
150
375
 
151
- client.send_cmd_and_wait(cmd) do |response|
152
- if response["result"]
153
- pdf_base64 = response["result"]["data"]
376
+ instrumentation_payload[:cmd] = cmd
154
377
 
155
- if outputfile
156
- raise PrintError, "Folder does not exist: #{File.dirname(outputfile)}" unless File.directory?(File.dirname(outputfile))
378
+ client.send_cmd_and_wait(cmd) do |response|
379
+ if response["result"]
380
+ pdf_base64 = response["result"]["data"]
157
381
 
158
- File.binwrite(outputfile, Base64.decode64(pdf_base64))
159
- Bidi2pdf.logger.info "PDF saved as '#{outputfile}'."
160
- else
161
- Bidi2pdf.logger.info "PDF generated successfully."
162
- end
382
+ instrumentation_payload[:pdf_base64] = pdf_base64
163
383
 
164
- block.call(pdf_base64) if block_given?
384
+ if outputfile
385
+ raise PrintError, "Folder does not exist: #{File.dirname(outputfile)}" unless File.directory?(File.dirname(outputfile))
165
386
 
166
- return pdf_base64 unless outputfile || block_given?
167
- else
168
- Bidi2pdf.logger.error "Error printing: #{response}"
387
+ File.binwrite(outputfile, Base64.decode64(pdf_base64))
388
+ Bidi2pdf.logger.info "PDF saved as '#{outputfile}'."
389
+ else
390
+ Bidi2pdf.logger.info "PDF generated successfully."
391
+ end
392
+
393
+ block.call(pdf_base64) if block_given?
394
+
395
+ return pdf_base64 unless outputfile || block_given?
396
+ else
397
+ Bidi2pdf.logger.error "Error printing: #{response}"
398
+ end
169
399
  end
170
400
  end
171
401
  end
172
402
 
173
- # rubocop: enable Metrics/AbcSize, Metrics/PerceivedComplexity
403
+ # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity
174
404
 
175
405
  private
176
406
 
407
+ def navigate_with_listeners(url)
408
+ register_event_listeners
409
+
410
+ cmd = Bidi2pdf::Bidi::Commands::BrowsingContextNavigate.new url: url, context: browsing_context_id
411
+
412
+ client.send_cmd_and_wait(cmd) do |response|
413
+ Bidi2pdf.logger.debug "Navigated to page url: #{url} response: #{response}"
414
+ end
415
+ rescue Bidi2pdf::CmdError => e
416
+ msg = e.response["message"]
417
+ case msg
418
+ when /^net::ERR_INVALID_AUTH_CREDENTIALS/
419
+ raise NavigationAuthError.new(url, msg)
420
+ when /^net::ERR_NAME_NOT_RESOLVED/
421
+ raise NavigationDNSError.new(url, msg)
422
+ when /^net::/
423
+ raise NavigationError, "Connection error: #{url} #{msg}"
424
+ else
425
+ raise e
426
+ end
427
+ end
428
+
429
+ def register_event_listeners
430
+ return if @event_handlers_registered
431
+
432
+ @event_handlers_registered = true
433
+
434
+ client.on_event("network.beforeRequestSent", "network.responseStarted", "network.responseCompleted", "network.fetchError",
435
+ &network_events.method(:handle_event))
436
+
437
+ client.on_event("log.entryAdded",
438
+ &logger_events.method(:handle_event))
439
+
440
+ client.on_event("browsingContext.navigationFailed", &navigation_failed_events.method(:handle_event))
441
+ end
442
+
443
+ def handle_injection_exception(response, url, exception_class)
444
+ exception = response["exceptionDetails"]
445
+ error_text = exception["text"]
446
+ line = exception["lineNumber"]
447
+ column = exception["columnNumber"]
448
+
449
+ # Extract stack trace information if available
450
+ stack_info = format_stack_trace(exception["stackTrace"])
451
+ script_source = url ? "URL: #{url}" : "inline content"
452
+ error_message = "Script injection failed (#{script_source}): #{error_text} at line #{line}:#{column}\n#{stack_info}"
453
+
454
+ Bidi2pdf.logger.error error_message
455
+ raise exception_class, error_message
456
+ end
457
+
458
+ # Generates JavaScript code for creating a script element with given parameters.
459
+ #
460
+ # @param [String, nil] url The URL of the script to load (optional).
461
+ # @param [String, nil] content The JavaScript content for the script (optional).
462
+ # @param [String, nil] id The ID attribute for the script element (optional).
463
+ # @return [String] JavaScript code that creates a script element.
464
+ def generate_script_element_code(url: nil, content: nil, id: nil)
465
+ js_src_part = ""
466
+ js_src_part = <<~SRC if url
467
+ script.src = '#{url}';
468
+ script.addEventListener(
469
+ 'load',
470
+ () => {
471
+ resolve(script);
472
+ },
473
+ {once: true},
474
+ );
475
+ SRC
476
+
477
+ <<~JS
478
+ new Promise((resolve, reject) => {
479
+ const script = document.createElement('script');
480
+ script.type = 'text/javascript';
481
+
482
+ #{content ? "script.text = #{content.to_json};" : ""}
483
+
484
+ script.addEventListener(
485
+ 'error',
486
+ event => {
487
+ reject(new Error(event.message ?? 'Could not load script'));
488
+ },
489
+ {once: true},
490
+ );
491
+
492
+ #{id ? "script.id = '#{id}';" : ""}
493
+ #{js_src_part}
494
+
495
+ document.head.appendChild(script);
496
+
497
+ #{url ? "" : "resolve(script);"}
498
+ });
499
+ JS
500
+ end
501
+
502
+ # Generates JavaScript code for creating a style element with given parameters.
503
+ #
504
+ # @param [String, nil] url The URL of the stylesheet to load (optional).
505
+ # @param [String, nil] content The CSS content for the style (optional).
506
+ # @param [String, nil] id The ID attribute for the style element (optional).
507
+ # @return [String] JavaScript code that creates a style element.
508
+ def generate_style_element_code(url: nil, content: nil, id: nil)
509
+ if url
510
+ # For external stylesheets, create a link element
511
+ <<~JS
512
+ new Promise((resolve, reject) => {
513
+ const link = document.createElement('link');
514
+ link.rel = 'stylesheet';
515
+ link.type = 'text/css';
516
+ link.href = '#{url}';
517
+ #{" "}
518
+ #{id ? "link.id = '#{id}';" : ""}
519
+ #{" "}
520
+ link.addEventListener(
521
+ 'load',
522
+ () => {
523
+ resolve(link);
524
+ },
525
+ {once: true}
526
+ );
527
+ #{" "}
528
+ link.addEventListener(
529
+ 'error',
530
+ event => {
531
+ reject(new Error(event.message ?? 'Could not load stylesheet'));
532
+ },
533
+ {once: true}
534
+ );
535
+ #{" "}
536
+ document.head.appendChild(link);
537
+ });
538
+ JS
539
+ else
540
+ # For inline styles, create a style element
541
+ <<~JS
542
+ new Promise((resolve, reject) => {
543
+ try {
544
+ const style = document.createElement('style');
545
+ style.type = 'text/css';
546
+ #{" "}
547
+ #{id ? "style.id = '#{id}';" : ""}
548
+ #{" "}
549
+ #{content ? "style.textContent = #{content.to_json};" : ""}
550
+ #{" "}
551
+ document.head.appendChild(style);
552
+ resolve(style);
553
+ } catch (error) {
554
+ reject(error);
555
+ }
556
+ });
557
+ JS
558
+ end
559
+ end
560
+
561
+ # Closes the browsing context.
177
562
  def close_context
178
563
  that = self
179
564
  cmd = Bidi2pdf::Bidi::Commands::BrowsingContextClose.new context: browsing_context_id
@@ -182,13 +567,18 @@ module Bidi2pdf
182
567
  end
183
568
  end
184
569
 
570
+ # Removes event listeners for the browser tab.
185
571
  def remove_event_listeners
186
- Bidi2pdf.logger.debug "Network events: #{network_events.all_events.map(&:to_s)}"
572
+ Bidi2pdf.logger.debug2 "Network events: #{network_events.all_events.map(&:to_s)}"
187
573
 
188
574
  client.remove_event_listener "network.responseStarted", "network.responseCompleted", "network.fetchError",
189
575
  &network_events.method(:handle_event)
576
+
577
+ client.remove_event_listener("log.entryAdded",
578
+ &logger_events.method(:handle_event))
190
579
  end
191
580
 
581
+ # Closes all tabs associated with the browser tab.
192
582
  def close_tabs
193
583
  tabs.each do |tab|
194
584
  tab.close