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
@@ -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
|
-
|
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.
|
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.
|
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
|
-
|
84
|
-
|
176
|
+
begin
|
177
|
+
URI.parse(url)
|
178
|
+
rescue URI::InvalidURIError => e
|
179
|
+
raise NavigationError, "Invalid URL: #{url} - #{e.message}"
|
180
|
+
end
|
85
181
|
|
86
|
-
|
87
|
-
|
182
|
+
Bidi2pdf.notification_service.instrument("navigate_to.bidi2pdf", url: url) do
|
183
|
+
navigate_with_listeners url
|
184
|
+
end
|
185
|
+
end
|
88
186
|
|
89
|
-
|
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
|
-
|
92
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
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
|
-
|
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
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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, &
|
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
|
-
#
|
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
|
-
|
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
|
-
|
152
|
-
if response["result"]
|
153
|
-
pdf_base64 = response["result"]["data"]
|
376
|
+
instrumentation_payload[:cmd] = cmd
|
154
377
|
|
155
|
-
|
156
|
-
|
378
|
+
client.send_cmd_and_wait(cmd) do |response|
|
379
|
+
if response["result"]
|
380
|
+
pdf_base64 = response["result"]["data"]
|
157
381
|
|
158
|
-
|
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
|
-
|
384
|
+
if outputfile
|
385
|
+
raise PrintError, "Folder does not exist: #{File.dirname(outputfile)}" unless File.directory?(File.dirname(outputfile))
|
165
386
|
|
166
|
-
|
167
|
-
|
168
|
-
|
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:
|
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.
|
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
|