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.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +63 -8
- data/README.md +28 -0
- data/docker/Dockerfile +1 -1
- data/docker/Dockerfile.chromedriver +9 -2
- data/docker/Dockerfile.slim +2 -2
- data/lib/bidi2pdf/bidi/browser_console_logger.rb +92 -0
- data/lib/bidi2pdf/bidi/browser_tab.rb +431 -41
- data/lib/bidi2pdf/bidi/client.rb +85 -23
- data/lib/bidi2pdf/bidi/command_manager.rb +46 -60
- data/lib/bidi2pdf/bidi/commands/base.rb +39 -1
- data/lib/bidi2pdf/bidi/commands/browser_remove_user_context.rb +27 -0
- data/lib/bidi2pdf/bidi/commands/browsing_context_print.rb +4 -0
- data/lib/bidi2pdf/bidi/commands/print_parameters_validator.rb +5 -0
- data/lib/bidi2pdf/bidi/commands.rb +1 -0
- data/lib/bidi2pdf/bidi/connection_manager.rb +3 -9
- data/lib/bidi2pdf/bidi/event_manager.rb +2 -2
- data/lib/bidi2pdf/bidi/interceptor.rb +1 -1
- data/lib/bidi2pdf/bidi/js_logger_helper.rb +16 -0
- data/lib/bidi2pdf/bidi/logger_events.rb +25 -45
- data/lib/bidi2pdf/bidi/navigation_failed_events.rb +41 -0
- data/lib/bidi2pdf/bidi/network_event.rb +15 -0
- data/lib/bidi2pdf/bidi/network_event_formatters/network_event_console_formatter.rb +4 -3
- data/lib/bidi2pdf/bidi/network_events.rb +27 -17
- data/lib/bidi2pdf/bidi/session.rb +123 -13
- data/lib/bidi2pdf/bidi/user_context.rb +62 -0
- data/lib/bidi2pdf/bidi/web_socket_dispatcher.rb +7 -7
- data/lib/bidi2pdf/chromedriver_manager.rb +48 -21
- data/lib/bidi2pdf/cli.rb +10 -2
- data/lib/bidi2pdf/dsl.rb +33 -0
- data/lib/bidi2pdf/launcher.rb +30 -0
- data/lib/bidi2pdf/notifications/event.rb +52 -0
- data/lib/bidi2pdf/notifications/instrumenter.rb +65 -0
- data/lib/bidi2pdf/notifications/logging_subscriber.rb +136 -0
- data/lib/bidi2pdf/notifications.rb +78 -0
- data/lib/bidi2pdf/session_runner.rb +35 -3
- data/lib/bidi2pdf/test_helpers/matchers/contains_pdf_text.rb +50 -0
- data/lib/bidi2pdf/test_helpers/matchers/have_pdf_page_count.rb +50 -0
- data/lib/bidi2pdf/test_helpers/matchers/match_pdf_text.rb +45 -0
- data/lib/bidi2pdf/test_helpers/pdf_reader_utils.rb +89 -0
- data/lib/bidi2pdf/test_helpers/pdf_text_sanitizer.rb +232 -0
- data/lib/bidi2pdf/test_helpers/testcontainers/chromedriver_container.rb +87 -0
- data/lib/bidi2pdf/test_helpers.rb +13 -0
- data/lib/bidi2pdf/verbose_logger.rb +79 -0
- data/lib/bidi2pdf/version.rb +1 -1
- data/lib/bidi2pdf.rb +131 -10
- data/sig/bidi2pdf/bidi/client.rbs +1 -1
- metadata +67 -4
- 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
|
-
|
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 =
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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|
|
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.
|
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
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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.
|
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
|
-
|
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
|
-
|
48
|
-
Bidi2pdf.
|
49
|
-
|
50
|
-
|
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
|
-
|
53
|
-
|
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)
|
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
|
-
|
83
|
-
|
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
|
-
#
|
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:
|
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(&
|
25
|
+
def on_message(&) = socket_events.on(:message, &)
|
26
26
|
|
27
|
-
def on_event(name, &
|
27
|
+
def on_event(name, &) = session_events.on(name, &)
|
28
28
|
|
29
|
-
def on_open(&
|
29
|
+
def on_open(&) = socket_events.on(:open, &)
|
30
30
|
|
31
|
-
def on_close(&
|
31
|
+
def on_close(&) = socket_events.on(:close, &)
|
32
32
|
|
33
|
-
def on_error(&
|
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.
|
55
|
+
Bidi2pdf.logger.debug3 "Dispatching session event: #{method}"
|
56
56
|
that.session_events.dispatch(method, data)
|
57
57
|
else
|
58
|
-
Bidi2pdf.logger.
|
58
|
+
Bidi2pdf.logger.debug3 "Dispatching socket message"
|
59
59
|
that.socket_events.dispatch(:message, data)
|
60
60
|
end
|
61
61
|
end
|