bidi2pdf 0.1.5 → 0.1.7

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