bidi2pdf 0.1.4 → 0.1.6

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.
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "network_event"
4
+
5
+ module Bidi2pdf
6
+ module Bidi
7
+ class LoggerEvents
8
+ attr_reader :context_id
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.dig("source", "context") == context_id
19
+ handle_response(method, event)
20
+ else
21
+ Bidi2pdf.logger.debug "Ignoring Log event: #{method}, context_id: #{context_id}, params: #{event}"
22
+ end
23
+ rescue StandardError => e
24
+ Bidi2pdf.logger.error "Error handling Log event: #{e.message}\n#{e.backtrace&.join("\n")}"
25
+ end
26
+
27
+ def handle_response(_method, event)
28
+ level = resolve_log_level(event["level"])
29
+ text = event["text"]
30
+ args = event["args"] || []
31
+ 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")
70
+ end
71
+
72
+ def resolve_log_level(js_level)
73
+ case js_level
74
+ when "info", "warn", "error"
75
+ js_level.to_sym
76
+ else
77
+ :debug
78
+ end
79
+ end
80
+
81
+ def log_prefix(timestamp)
82
+ "[#{timestamp}][Browser Console Log]"
83
+ end
84
+ end
85
+ end
86
+ end
@@ -3,7 +3,8 @@
3
3
  module Bidi2pdf
4
4
  module Bidi
5
5
  class NetworkEvent
6
- attr_reader :id, :url, :state, :start_timestamp, :end_timestamp, :timing
6
+ attr_reader :id, :url, :state, :start_timestamp, :end_timestamp, :timing, :http_status_code,
7
+ :http_method, :bytes_received
7
8
 
8
9
  STATE_MAP = {
9
10
  "network.responseStarted" => "started",
@@ -11,18 +12,22 @@ module Bidi2pdf
11
12
  "network.fetchError" => "error"
12
13
  }.freeze
13
14
 
14
- def initialize(id:, url:, timestamp:, timing:, state:)
15
+ def initialize(id:, url:, timestamp:, timing:, state:, http_status_code: nil, http_method: nil)
15
16
  @id = id
16
17
  @url = url
17
18
  @start_timestamp = timestamp
18
19
  @timing = timing
19
20
  @state = map_state(state)
21
+ @http_status_code = http_status_code
22
+ @http_method = http_method
20
23
  end
21
24
 
22
- def update_state(new_state, timestamp: nil, timing: nil)
25
+ def update_state(new_state, timestamp: nil, timing: nil, http_status_code: nil, bytes_received: nil)
23
26
  @state = map_state(new_state)
24
27
  @end_timestamp = timestamp if timestamp
25
28
  @timing = timing if timing
29
+ @http_status_code = http_status_code if http_status_code
30
+ @bytes_received = bytes_received if bytes_received
26
31
  end
27
32
 
28
33
  def map_state(state)
@@ -44,10 +49,23 @@ module Bidi2pdf
44
49
  def in_progress? = state == "started"
45
50
 
46
51
  def to_s
47
- took_str = duration_seconds ? "took #{duration_seconds} sec" : "in progress"
48
- "#<NetworkEvent id=#{@id} url=#{@url} state=#{@state} " \
49
- "start=#{format_timestamp(@start_timestamp)} " \
50
- "end=#{format_timestamp(@end_timestamp)} #{took_str}>"
52
+ took_str = duration_seconds ? "#{duration_seconds.round(2)} sec" : "in progress"
53
+ http_status = @http_status_code ? "HTTP #{@http_status_code}" : "HTTP (N/A)"
54
+ start_str = format_timestamp(@start_timestamp) || "N/A"
55
+ end_str = format_timestamp(@end_timestamp) || "N/A"
56
+ method_str = @http_method || "N/A"
57
+ bytes_str = @bytes_received ? "#{@bytes_received} bytes" : "0 bytes"
58
+
59
+ "#<NetworkEvent " \
60
+ "id=#{@id.inspect}, " \
61
+ "method=#{method_str.inspect}, " \
62
+ "url=#{@url.inspect}, " \
63
+ "state=#{@state.inspect}, " \
64
+ "#{http_status}, " \
65
+ "bytes_received=#{bytes_str}, " \
66
+ "start=#{start_str}, " \
67
+ "end=#{end_str}, " \
68
+ "duration=#{took_str}>"
51
69
  end
52
70
  end
53
71
  end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdf
4
+ module Bidi
5
+ module NetworkEventFormatters
6
+ # rubocop: disable Metrics/AbcSize
7
+ class NetworkEventConsoleFormatter
8
+ include NetworkEventFormatterUtils
9
+
10
+ attr_reader :color_enabled
11
+
12
+ # ANSI styles
13
+ RESET = "\e[0m"
14
+ BOLD = "\e[1m"
15
+ DIM = "\e[2m"
16
+ RED = "\e[31m"
17
+ GREEN = "\e[32m"
18
+ YELLOW = "\e[33m"
19
+ CYAN = "\e[36m"
20
+ GRAY = "\e[90m"
21
+
22
+ def initialize(color: true)
23
+ @color_enabled = color
24
+ end
25
+
26
+ def log(events)
27
+ events.each { |event| pretty_log(event).each_line { |line| Bidi2pdf.network_events_logger.info(line.chomp) } }
28
+ end
29
+
30
+ def pretty_log(event)
31
+ status = event.http_status_code ? "HTTP #{event.http_status_code}" : "pending"
32
+ status_color = color_for_status(event.http_status_code)
33
+ start = event.format_timestamp(event.start_timestamp)
34
+ finish = event.end_timestamp ? event.format_timestamp(event.end_timestamp) : dim("...")
35
+ duration = event.duration_seconds ? cyan("#{event.duration_seconds}s") : dim("in progress")
36
+ timing_details = format_timing(event)
37
+ bytes = event.bytes_received ? format_bytes(event.bytes_received) : dim("N/A")
38
+ displayed_url = shorten_url(event.url)
39
+
40
+ <<~LOG.strip
41
+ #{bold("┌─ Network Event ──────────────────────────────────────")}
42
+ #{bold("│ Request: ")}#{event.http_method || "?"} #{displayed_url}#{" "}
43
+ #{bold("│ State: ")}#{event.state}
44
+ #{bold("│ Status: ")}#{status_color}#{status}#{reset}
45
+ #{bold("│ Started: ")}#{start}
46
+ #{bold("│ Ended: ")}#{finish}
47
+ #{bold("│ Duration:")} #{duration}
48
+ #{bold("│ Received:")} #{bytes}
49
+ #{timing_details}
50
+ #{bold("└──────────────────────────────────────────────────────")}
51
+ LOG
52
+ end
53
+
54
+ private
55
+
56
+ def format_timing(event)
57
+ return "" unless event.timing.is_a?(Hash)
58
+
59
+ keys = %w[
60
+ requestTime proxyStart proxyEnd dnsStart dnsEnd connectStart connectEnd
61
+ sslStart sslEnd workerStart workerReady sendStart sendEnd receiveHeadersEnd
62
+ ]
63
+
64
+ visible = keys.map do |key|
65
+ next unless event.timing[key]
66
+
67
+ label = key.gsub(/([A-Z])/, ' \1').capitalize
68
+ "#{dim("│")} #{label.ljust(20)}: #{event.timing[key].round(2)} ms#{reset}"
69
+ end.compact
70
+
71
+ return "" if visible.empty?
72
+
73
+ [dim("│").to_s, dim("│ Timing Phases:").to_s].concat(visible).join("\n")
74
+ end
75
+
76
+ # === Color Helpers ===
77
+
78
+ def color_for_status(code)
79
+ return gray unless code
80
+
81
+ case code.to_i
82
+ when 200..299 then green
83
+ when 300..499 then yellow
84
+ when 500..599 then red
85
+ else gray
86
+ end
87
+ end
88
+
89
+ def bold(str) = color_enabled ? "#{BOLD}#{str}#{RESET}" : str
90
+
91
+ def dim(str) = color_enabled ? "#{DIM}#{str}#{RESET}" : str
92
+
93
+ def green(str = "") = color_enabled ? "#{GREEN}#{str}" : str
94
+
95
+ def yellow(str = "") = color_enabled ? "#{YELLOW}#{str}" : str
96
+
97
+ def red(str = "") = color_enabled ? "#{RED}#{str}" : str
98
+
99
+ def cyan(str = "") = color_enabled ? "#{CYAN}#{str}" : str
100
+
101
+ def gray(str = "") = color_enabled ? "#{GRAY}#{str}" : str
102
+
103
+ def reset = color_enabled ? RESET : ""
104
+
105
+ # rubocop: enable Metrics/AbcSize
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ module Bidi2pdf
6
+ module Bidi
7
+ module NetworkEventFormatters
8
+ module NetworkEventFormatterUtils
9
+ def format_bytes(size)
10
+ return "N/A" unless size.is_a?(Numeric)
11
+
12
+ units = %w[B KB MB GB TB]
13
+ idx = 0
14
+ while size >= 1024 && idx < units.size - 1
15
+ size /= 1024.0
16
+ idx += 1
17
+ end
18
+ format("%<size>.2f %<unit>s", size: size, unit: units[idx])
19
+ end
20
+
21
+ def parse_timing(event)
22
+ return [] unless event.timing.is_a?(Hash)
23
+
24
+ keys = %w[
25
+ requestTime proxyStart proxyEnd dnsStart dnsEnd connectStart connectEnd
26
+ sslStart sslEnd workerStart workerReady sendStart sendEnd receiveHeadersEnd
27
+ ]
28
+
29
+ keys.filter_map do |key|
30
+ next unless event.timing[key]
31
+
32
+ label = key.gsub(/([A-Z])/, ' \1').capitalize
33
+ { label: label, key: key, ms: event.timing[key].round(2) }
34
+ end
35
+ end
36
+
37
+ def format_timestamp(timestamp)
38
+ return "N/A" unless timestamp
39
+
40
+ Time.at(timestamp.to_f / 1000).utc.strftime("%Y-%m-%d %H:%M:%S.%L UTC")
41
+ end
42
+
43
+ def shorten_url(url)
44
+ sanitized_url = CGI.escapeHTML(url)
45
+
46
+ return sanitized_url unless sanitized_url.start_with?("data:text/html")
47
+
48
+ "data:text/html,..."
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdf
4
+ module Bidi
5
+ module NetworkEventFormatters
6
+ class NetworkEventHtmlFormatter
7
+ include NetworkEventFormatterUtils
8
+
9
+ def render(events)
10
+ return unless Bidi2pdf.network_events_logger.info?
11
+
12
+ <<~HTML
13
+ <!DOCTYPE html>
14
+ <html lang="en" data-bs-theme="light">
15
+ <head>
16
+ <meta charset="UTF-8">
17
+ <title>Network Events</title>
18
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
19
+ <style>
20
+ body { font-family: monospace; padding: 2rem; }
21
+ .event { background: var(--bs-body-bg); border: 1px solid var(--bs-border-color); padding: 1rem; margin-bottom: 2rem; border-radius: .5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
22
+ .bar-container { position: relative; height: 20px; background: var(--bs-secondary-bg); border-radius: 4px; }
23
+ .bar { position: absolute; top: 0; height: 100%; background-color: #0d6efd; opacity: 0.8; }
24
+ .toc a { text-decoration: none; display: block; margin-bottom: 0.5rem; }
25
+
26
+ @media print {
27
+ .no-break {
28
+ page-break-inside: avoid;
29
+ }
30
+ #{" "}
31
+ #{" "}
32
+ .form-select, .form-label, #theme-select {
33
+ display: none !important; /* Hide theme selector when printing */
34
+ }
35
+ }
36
+
37
+ .event {
38
+ word-break: break-word;
39
+ overflow-wrap: anywhere;
40
+ }
41
+ </style>
42
+ <script>
43
+ function toggleTheme(value) {
44
+ document.documentElement.setAttribute('data-bs-theme', value);
45
+ }
46
+ </script>
47
+ </head>
48
+ <body>
49
+ <h1>Network Events</h1>
50
+ <div class="mb-4">
51
+ <label for="theme-select" class="form-label">Theme:</label>
52
+ <select id="theme-select" class="form-select w-auto d-inline-block" onchange="toggleTheme(this.value)">
53
+ <option value="light">Light</option>
54
+ <option value="dark">Dark</option>
55
+ </select>
56
+ </div>
57
+
58
+ <h2>Index</h2>
59
+ <div class="toc mb-4">
60
+ #{events.map.with_index { |e, i| toc_entry(e, i) }.join("\n")}
61
+ </div>
62
+
63
+ #{events.map.with_index { |e, i| render_event(e, i) }.join("\n")}
64
+ </body>
65
+ </html>
66
+ HTML
67
+ end
68
+
69
+ def toc_entry(event, index)
70
+ "<a href=\"#event-#{index}\">[#{index + 1}] #{event.http_method} #{event.url}</a>"
71
+ end
72
+
73
+ # rubocop: disable Metrics/AbcSize
74
+ def render_event(event, index)
75
+ timing = parse_timing(event)
76
+ duration = event.duration_seconds || 0
77
+ duration_str = event.in_progress? ? "in progress" : "#{duration}s"
78
+ status = event.http_status_code || "?"
79
+ method = event.http_method || "?"
80
+ start = format_timestamp(event.start_timestamp)
81
+ finish = event.end_timestamp ? format_timestamp(event.end_timestamp) : "..."
82
+ bytes = event.bytes_received ? format_bytes(event.bytes_received) : "N/A"
83
+ bars = render_timing_bars(timing)
84
+ displayed_url = shorten_url(event.url)
85
+
86
+ <<~HTML
87
+ <div class="event no-break" id="event-#{index}">
88
+ <div><strong>Request:</strong> #{method} #{displayed_url}</div>
89
+ <div><strong>Status:</strong> HTTP #{status}</div>
90
+ <div><strong>State:</strong> #{event.state}</div>
91
+ <div><strong>Start:</strong> #{start}</div>
92
+ <div><strong>End:</strong> #{finish}</div>
93
+ <div><strong>Duration:</strong> #{duration_str}</div>
94
+ <div><strong>Received:</strong> #{bytes}</div>
95
+ #{bars}
96
+ </div>
97
+ HTML
98
+ end
99
+
100
+ # rubocop: enable Metrics/AbcSize
101
+
102
+ def render_timing_bars(timing)
103
+ return "" if timing.empty?
104
+
105
+ max_ms = timing.map { |t| t[:ms] }.max
106
+ scale = max_ms.zero? ? 0 : 100.0 / max_ms
107
+
108
+ bars = timing.map do |t|
109
+ width = (t[:ms] * scale).clamp(1, 100).round(2)
110
+ <<~HTML
111
+ <div>
112
+ <small>#{t[:label]} (#{t[:ms]} ms)</small>
113
+ <div class="bar-container mb-2">
114
+ <div class="bar" style="width: #{width}%"></div>
115
+ </div>
116
+ </div>
117
+ HTML
118
+ end
119
+
120
+ "<div class=\"mt-3\"><strong>Timing Waterfall</strong>#{bars.join}</div>"
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdf
4
+ module Bidi
5
+ module NetworkEventFormatters
6
+ require_relative "network_event_formatters/network_event_formatter_utils"
7
+ require_relative "network_event_formatters/network_event_console_formatter"
8
+ require_relative "network_event_formatters/network_event_html_formatter"
9
+ end
10
+ end
11
+ end
@@ -1,15 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "network_event"
4
+ require_relative "network_event_formatters"
4
5
 
5
6
  module Bidi2pdf
6
7
  module Bidi
7
8
  class NetworkEvents
8
- attr_reader :context_id, :events
9
+ attr_reader :context_id, :events, :network_event_formatter
9
10
 
10
11
  def initialize(context_id)
11
12
  @context_id = context_id
12
13
  @events = {}
14
+ @network_event_formatter = NetworkEventFormatters::NetworkEventConsoleFormatter.new
13
15
  end
14
16
 
15
17
  def handle_event(data)
@@ -30,23 +32,28 @@ module Bidi2pdf
30
32
  return unless event && event["request"]
31
33
 
32
34
  request = event["request"]
35
+ response = event["response"]
36
+ http_status_code = response&.dig("status")
37
+ bytes_received = response&.dig("bytesReceived")
33
38
 
34
39
  id = request["request"]
35
40
  url = request["url"]
36
41
  timing = request["timings"]
42
+ http_method = request["method"]
37
43
 
38
44
  timestamp = event["timestamp"]
39
45
 
40
- if method == "network.responseStarted"
46
+ if method == "network.beforeRequestSent"
41
47
  events[id] ||= NetworkEvent.new(
42
48
  id: id,
43
49
  url: url,
44
50
  timestamp: timestamp,
45
51
  timing: timing,
46
- state: method
52
+ state: method,
53
+ http_method: http_method
47
54
  )
48
55
  elsif events.key?(id)
49
- events[id].update_state(method, timestamp: timestamp, timing: timing)
56
+ events[id].update_state(method, timestamp: timestamp, timing: timing, http_status_code: http_status_code, bytes_received: bytes_received)
50
57
  else
51
58
  Bidi2pdf.logger.warn "Received response for unknown request ID: #{id}, URL: #{url}"
52
59
  end
@@ -58,7 +65,19 @@ module Bidi2pdf
58
65
  events.values.sort_by(&:start_timestamp)
59
66
  end
60
67
 
61
- def wait_until_all_finished(timeout: 10, poll_interval: 0.1)
68
+ def log_network_traffic(format: :console)
69
+ format = format.to_sym
70
+
71
+ if format == :console
72
+ NetworkEventFormatters::NetworkEventConsoleFormatter.new.log all_events
73
+ elsif format == :html
74
+ NetworkEventFormatters::NetworkEventHtmlFormatter.new.render(all_events)
75
+ else
76
+ raise ArgumentError, "Unknown network event format: #{format}"
77
+ end
78
+ end
79
+
80
+ def wait_until_network_idle(timeout: 10, poll_interval: 0.1)
62
81
  start_time = Time.now
63
82
 
64
83
  loop do
@@ -10,7 +10,7 @@ require_relative "user_context"
10
10
  module Bidi2pdf
11
11
  module Bidi
12
12
  class Session
13
- SUBSCRIBE_EVENTS = %w[log script].freeze
13
+ SUBSCRIBE_EVENTS = %w[script].freeze
14
14
  DEFAULT_CHROME_ARGS = %w[--disable-gpu --disable-popup-blocking --disable-hang-monitor].freeze
15
15
 
16
16
  attr_reader :session_uri, :started, :chrome_args
data/lib/bidi2pdf/cli.rb CHANGED
@@ -31,6 +31,10 @@ module Bidi2pdf
31
31
  USAGE
32
32
 
33
33
  option :url, desc: "The URL to render"
34
+ # of course, it's possible to render a local file via: --url file:///path/to/the/file.html
35
+ # but this should showcase the scenario that you render a string within ruby as pdf without the need
36
+ # to store it on disc
37
+ option :html_file, desc: "The local HTML file to render"
34
38
  option :output, default: "output.pdf", desc: "Filename for the output PDF", aliases: "-o"
35
39
  option :cookie, type: :array, default: [], banner: "name=value", desc: "One or more cookies", aliases: "-C"
36
40
  option :header, type: :array, default: [], banner: "name=value", desc: "One or more custom headers", aliases: "-H"
@@ -47,6 +51,12 @@ module Bidi2pdf
47
51
  option :log_level,
48
52
  type: :string,
49
53
  default: "info", enum: %w[debug info warn error fatal unknown], desc: "Set log level"
54
+ option :log_network_traffic, type: :boolean, default: false, desc: "Log network traffic", aliases: "-n"
55
+ option :network_log_format,
56
+ type: :string,
57
+ default: "console",
58
+ enum: %w[console pdf],
59
+ desc: "Choose network log format: console or pdf", aliases: "-f"
50
60
 
51
61
  option :background, type: :boolean, default: true, desc: "Print background graphics"
52
62
  option :margin_top, type: :numeric, default: 1.0, desc: "Top margin in inches"
@@ -60,10 +70,16 @@ module Bidi2pdf
60
70
  option :scale, type: :numeric, default: 1.0, desc: "Scale between 0.1 and 2.0"
61
71
  option :shrink_to_fit, type: :boolean, default: true, desc: "Shrink content to fit page"
62
72
 
73
+ class << self
74
+ def exit_on_failure?
75
+ true
76
+ end
77
+ end
78
+
63
79
  def render
64
80
  load_config
65
81
 
66
- validate_required_options! :url
82
+ validate_required_options!
67
83
 
68
84
  configure
69
85
 
@@ -115,9 +131,11 @@ module Bidi2pdf
115
131
  YAML.load_file(options[:config]).transform_keys(&:to_sym)
116
132
  end
117
133
 
118
- def validate_required_options!(*keys)
119
- keys.each do |key|
120
- raise Thor::Error, "Missing required option: --#{key.to_s.tr("_", "-")}" unless merged_options[key]
134
+ def validate_required_options!
135
+ if merged_options[:url].nil? && merged_options[:html_file].nil?
136
+ raise Thor::Error, "Missing required option --url or --html_file needs to be specified"
137
+ elsif merged_options[:html_file] && !File.readable?(merged_options[:html_file])
138
+ raise Thor::Error, "HTML file '#{merged_options[:html_file]}' not found or not readable"
121
139
  end
122
140
  end
123
141
 
@@ -182,6 +200,7 @@ module Bidi2pdf
182
200
 
183
201
  Bidi2pdf::Launcher.new(
184
202
  url: merged_options[:url],
203
+ inputfile: merged_options[:hmtl_file],
185
204
  output: merged_options[:output],
186
205
  cookies: parse_key_values(merged_options[:cookie]),
187
206
  headers: parse_key_values(merged_options[:header]),
@@ -191,7 +210,8 @@ module Bidi2pdf
191
210
  headless: merged_options[:headless],
192
211
  wait_window_loaded: merged_options[:wait_window_loaded],
193
212
  wait_network_idle: merged_options[:wait_network_idle],
194
- print_options: print_options
213
+ print_options: print_options,
214
+ network_log_format: merged_options[:network_log_format]
195
215
  )
196
216
  end
197
217
  end
@@ -201,6 +221,9 @@ module Bidi2pdf
201
221
  def configure
202
222
  Bidi2pdf.configure do |config|
203
223
  config.logger.level = log_level
224
+
225
+ config.network_events_logger.level = Logger::INFO if merged_options[:log_network_traffic]
226
+
204
227
  config.default_timeout = merged_options[:default_timeout]
205
228
 
206
229
  Chromedriver::Binary.configure do |c|
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bidi2pdf"
4
+
5
+ module Bidi2pdf
6
+ module DSL
7
+ # rubocop: disable Metrics/AbcSize
8
+ def self.with_tab(remote_browser_url: nil, port: 0, headless: true, chrome_args: Bidi2pdf::Bidi::Session::DEFAULT_CHROME_ARGS.dup)
9
+ manager = nil
10
+ session = nil
11
+ tab = nil
12
+
13
+ begin
14
+ session = if remote_browser_url
15
+ Bidi2pdf::Bidi::Session.new(
16
+ session_url: remote_browser_url,
17
+ headless: true, # remote is always headless
18
+ chrome_args: chrome_args
19
+ )
20
+ else
21
+ manager = Bidi2pdf::ChromedriverManager.new(port: port, headless: headless)
22
+ manager.start
23
+ manager.session
24
+ end
25
+
26
+ session.start
27
+ session.client.on_close { Bidi2pdf.logger.info "WebSocket session closed" }
28
+
29
+ browser = session.browser
30
+ context = browser.create_user_context
31
+ window = context.create_browser_window
32
+ tab = window.create_browser_tab
33
+
34
+ yield(tab)
35
+ ensure
36
+ tab&.close
37
+ window&.close
38
+ session&.close
39
+ manager&.stop
40
+ end
41
+ end
42
+
43
+ # rubocop: enable Metrics/AbcSize
44
+ end
45
+ end