puppeteer-bidi 0.0.1.beta10 → 0.0.1
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/AGENTS.md +44 -0
- data/API_COVERAGE.md +345 -0
- data/CLAUDE/porting_puppeteer.md +20 -0
- data/CLAUDE.md +2 -1
- data/DEVELOPMENT.md +14 -0
- data/README.md +47 -415
- data/development/generate_api_coverage.rb +411 -0
- data/development/puppeteer_revision.txt +1 -0
- data/lib/puppeteer/bidi/browser.rb +118 -22
- data/lib/puppeteer/bidi/browser_context.rb +185 -2
- data/lib/puppeteer/bidi/connection.rb +16 -5
- data/lib/puppeteer/bidi/cookie_utils.rb +192 -0
- data/lib/puppeteer/bidi/core/browsing_context.rb +83 -40
- data/lib/puppeteer/bidi/core/realm.rb +6 -0
- data/lib/puppeteer/bidi/core/request.rb +79 -35
- data/lib/puppeteer/bidi/core/user_context.rb +5 -3
- data/lib/puppeteer/bidi/element_handle.rb +200 -8
- data/lib/puppeteer/bidi/errors.rb +4 -0
- data/lib/puppeteer/bidi/frame.rb +115 -11
- data/lib/puppeteer/bidi/http_request.rb +577 -0
- data/lib/puppeteer/bidi/http_response.rb +161 -10
- data/lib/puppeteer/bidi/locator.rb +792 -0
- data/lib/puppeteer/bidi/page.rb +859 -7
- data/lib/puppeteer/bidi/query_handler.rb +1 -1
- data/lib/puppeteer/bidi/version.rb +1 -1
- data/lib/puppeteer/bidi.rb +39 -6
- data/sig/puppeteer/bidi/browser.rbs +53 -6
- data/sig/puppeteer/bidi/browser_context.rbs +36 -0
- data/sig/puppeteer/bidi/cookie_utils.rbs +64 -0
- data/sig/puppeteer/bidi/core/browsing_context.rbs +16 -6
- data/sig/puppeteer/bidi/core/request.rbs +14 -11
- data/sig/puppeteer/bidi/core/user_context.rbs +2 -2
- data/sig/puppeteer/bidi/element_handle.rbs +28 -0
- data/sig/puppeteer/bidi/errors.rbs +4 -0
- data/sig/puppeteer/bidi/frame.rbs +17 -0
- data/sig/puppeteer/bidi/http_request.rbs +162 -0
- data/sig/puppeteer/bidi/http_response.rbs +67 -8
- data/sig/puppeteer/bidi/locator.rbs +267 -0
- data/sig/puppeteer/bidi/page.rbs +170 -0
- data/sig/puppeteer/bidi.rbs +15 -3
- metadata +12 -1
data/lib/puppeteer/bidi/page.rb
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
-
require
|
|
5
|
-
require
|
|
4
|
+
require "base64"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "uri"
|
|
7
|
+
require "async/semaphore"
|
|
6
8
|
|
|
7
9
|
module Puppeteer
|
|
8
10
|
module Bidi
|
|
@@ -10,6 +12,58 @@ module Puppeteer
|
|
|
10
12
|
# This is a high-level wrapper around Core::BrowsingContext
|
|
11
13
|
class Page
|
|
12
14
|
DEFAULT_TIMEOUT = 30_000 #: Integer
|
|
15
|
+
UNIT_TO_PIXELS = {
|
|
16
|
+
"px" => 1,
|
|
17
|
+
"in" => 96,
|
|
18
|
+
"cm" => 37.8,
|
|
19
|
+
"mm" => 3.78
|
|
20
|
+
}.freeze
|
|
21
|
+
PAPER_FORMATS = {
|
|
22
|
+
"letter" => {
|
|
23
|
+
"cm" => { width: 21.59, height: 27.94 },
|
|
24
|
+
"in" => { width: 8.5, height: 11 }
|
|
25
|
+
},
|
|
26
|
+
"legal" => {
|
|
27
|
+
"cm" => { width: 21.59, height: 35.56 },
|
|
28
|
+
"in" => { width: 8.5, height: 14 }
|
|
29
|
+
},
|
|
30
|
+
"tabloid" => {
|
|
31
|
+
"cm" => { width: 27.94, height: 43.18 },
|
|
32
|
+
"in" => { width: 11, height: 17 }
|
|
33
|
+
},
|
|
34
|
+
"ledger" => {
|
|
35
|
+
"cm" => { width: 43.18, height: 27.94 },
|
|
36
|
+
"in" => { width: 17, height: 11 }
|
|
37
|
+
},
|
|
38
|
+
"a0" => {
|
|
39
|
+
"cm" => { width: 84.1, height: 118.9 },
|
|
40
|
+
"in" => { width: 33.1102, height: 46.811 }
|
|
41
|
+
},
|
|
42
|
+
"a1" => {
|
|
43
|
+
"cm" => { width: 59.4, height: 84.1 },
|
|
44
|
+
"in" => { width: 23.3858, height: 33.1102 }
|
|
45
|
+
},
|
|
46
|
+
"a2" => {
|
|
47
|
+
"cm" => { width: 42, height: 59.4 },
|
|
48
|
+
"in" => { width: 16.5354, height: 23.3858 }
|
|
49
|
+
},
|
|
50
|
+
"a3" => {
|
|
51
|
+
"cm" => { width: 29.7, height: 42 },
|
|
52
|
+
"in" => { width: 11.6929, height: 16.5354 }
|
|
53
|
+
},
|
|
54
|
+
"a4" => {
|
|
55
|
+
"cm" => { width: 21, height: 29.7 },
|
|
56
|
+
"in" => { width: 8.2677, height: 11.6929 }
|
|
57
|
+
},
|
|
58
|
+
"a5" => {
|
|
59
|
+
"cm" => { width: 14.8, height: 21 },
|
|
60
|
+
"in" => { width: 5.8268, height: 8.2677 }
|
|
61
|
+
},
|
|
62
|
+
"a6" => {
|
|
63
|
+
"cm" => { width: 10.5, height: 14.8 },
|
|
64
|
+
"in" => { width: 4.1339, height: 5.8268 }
|
|
65
|
+
}
|
|
66
|
+
}.freeze
|
|
13
67
|
|
|
14
68
|
attr_reader :browsing_context #: Core::BrowsingContext
|
|
15
69
|
attr_reader :browser_context #: BrowserContext
|
|
@@ -23,6 +77,15 @@ module Puppeteer
|
|
|
23
77
|
@browsing_context = browsing_context
|
|
24
78
|
@timeout_settings = TimeoutSettings.new(DEFAULT_TIMEOUT)
|
|
25
79
|
@emitter = Core::EventEmitter.new
|
|
80
|
+
@request_interception_semaphore = Async::Semaphore.new(1)
|
|
81
|
+
@request_handlers = begin
|
|
82
|
+
ObjectSpace::WeakMap.new
|
|
83
|
+
rescue NameError
|
|
84
|
+
{}
|
|
85
|
+
end
|
|
86
|
+
@request_interception = nil
|
|
87
|
+
@auth_interception = nil
|
|
88
|
+
@credentials = nil
|
|
26
89
|
end
|
|
27
90
|
|
|
28
91
|
# Event emitter delegation methods
|
|
@@ -33,7 +96,18 @@ module Puppeteer
|
|
|
33
96
|
# @rbs &block: (untyped) -> void -- Event handler
|
|
34
97
|
# @rbs return: void
|
|
35
98
|
def on(event, &block)
|
|
36
|
-
@emitter.on(event, &block)
|
|
99
|
+
return @emitter.on(event, &block) unless event.to_sym == :request
|
|
100
|
+
|
|
101
|
+
wrapper = @request_handlers[block]
|
|
102
|
+
unless wrapper
|
|
103
|
+
wrapper = lambda do |request|
|
|
104
|
+
request.enqueue_intercept_action do
|
|
105
|
+
block.call(request)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
@request_handlers[block] = wrapper
|
|
109
|
+
end
|
|
110
|
+
@emitter.on(event, &wrapper)
|
|
37
111
|
end
|
|
38
112
|
|
|
39
113
|
# Register a one-time event listener
|
|
@@ -49,6 +123,16 @@ module Puppeteer
|
|
|
49
123
|
# @rbs &block: (untyped) -> void -- Event handler to remove
|
|
50
124
|
# @rbs return: void
|
|
51
125
|
def off(event, &block)
|
|
126
|
+
if event.to_sym == :request && block
|
|
127
|
+
# WeakMap#delete was added in Ruby 3.3, so we need to handle older versions
|
|
128
|
+
wrapper = if @request_handlers.respond_to?(:delete)
|
|
129
|
+
@request_handlers.delete(block)
|
|
130
|
+
else
|
|
131
|
+
@request_handlers[block]
|
|
132
|
+
end
|
|
133
|
+
return @emitter.off(event, &(wrapper || block))
|
|
134
|
+
end
|
|
135
|
+
|
|
52
136
|
@emitter.off(event, &block)
|
|
53
137
|
end
|
|
54
138
|
|
|
@@ -60,6 +144,19 @@ module Puppeteer
|
|
|
60
144
|
@emitter.emit(event, data)
|
|
61
145
|
end
|
|
62
146
|
|
|
147
|
+
# @rbs return: bool -- Whether any network interception is enabled
|
|
148
|
+
def network_interception_enabled?
|
|
149
|
+
!@request_interception.nil? || !@auth_interception.nil?
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# @rbs return: Hash[Symbol, String]?
|
|
153
|
+
def credentials
|
|
154
|
+
@credentials
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# @rbs return: Async::Semaphore -- Serialize request interception handling
|
|
158
|
+
attr_reader :request_interception_semaphore
|
|
159
|
+
|
|
63
160
|
# Navigate to a URL
|
|
64
161
|
# @rbs url: String -- URL to navigate to
|
|
65
162
|
# @rbs wait_until: String -- When to consider navigation complete ('load', 'domcontentloaded')
|
|
@@ -181,12 +278,18 @@ module Puppeteer
|
|
|
181
278
|
}
|
|
182
279
|
end
|
|
183
280
|
|
|
281
|
+
clip_x, clip_y, clip_width, clip_height = process_clip(
|
|
282
|
+
clip_x: box[:x],
|
|
283
|
+
clip_y: box[:y],
|
|
284
|
+
clip_width: box[:width],
|
|
285
|
+
clip_height: box[:height],
|
|
286
|
+
)
|
|
184
287
|
options[:clip] = {
|
|
185
288
|
type: 'box',
|
|
186
|
-
x:
|
|
187
|
-
y:
|
|
188
|
-
width:
|
|
189
|
-
height:
|
|
289
|
+
x: clip_x,
|
|
290
|
+
y: clip_y,
|
|
291
|
+
width: clip_width,
|
|
292
|
+
height: clip_height
|
|
190
293
|
}
|
|
191
294
|
end
|
|
192
295
|
|
|
@@ -206,6 +309,115 @@ module Puppeteer
|
|
|
206
309
|
data
|
|
207
310
|
end
|
|
208
311
|
|
|
312
|
+
# Generate a PDF of the page.
|
|
313
|
+
# @rbs path: String? -- File path to save PDF
|
|
314
|
+
# @rbs scale: Numeric? -- Scale of the webpage rendering
|
|
315
|
+
# @rbs display_header_footer: bool? -- Display header and footer
|
|
316
|
+
# @rbs header_template: String? -- HTML template for header
|
|
317
|
+
# @rbs footer_template: String? -- HTML template for footer
|
|
318
|
+
# @rbs print_background: bool? -- Print background graphics
|
|
319
|
+
# @rbs landscape: bool? -- Print in landscape orientation
|
|
320
|
+
# @rbs page_ranges: String? -- Paper ranges to print (e.g. "1-5, 8")
|
|
321
|
+
# @rbs format: String? -- Paper format (e.g. "A4")
|
|
322
|
+
# @rbs width: String | Numeric? -- Paper width
|
|
323
|
+
# @rbs height: String | Numeric? -- Paper height
|
|
324
|
+
# @rbs prefer_css_page_size: bool? -- Prefer CSS @page size
|
|
325
|
+
# @rbs margin: Hash[Symbol, String | Numeric]? -- Paper margins
|
|
326
|
+
# @rbs omit_background: bool? -- Omit background
|
|
327
|
+
# @rbs tagged: bool? -- Generate tagged PDF
|
|
328
|
+
# @rbs outline: bool? -- Generate document outline
|
|
329
|
+
# @rbs timeout: Numeric? -- Timeout in ms (0 disables)
|
|
330
|
+
# @rbs wait_for_fonts: bool? -- Wait for document fonts to load
|
|
331
|
+
# @rbs return: String -- PDF data as binary string
|
|
332
|
+
def pdf(
|
|
333
|
+
path: nil,
|
|
334
|
+
scale: nil,
|
|
335
|
+
display_header_footer: nil,
|
|
336
|
+
header_template: nil,
|
|
337
|
+
footer_template: nil,
|
|
338
|
+
print_background: nil,
|
|
339
|
+
landscape: nil,
|
|
340
|
+
page_ranges: nil,
|
|
341
|
+
format: nil,
|
|
342
|
+
width: nil,
|
|
343
|
+
height: nil,
|
|
344
|
+
prefer_css_page_size: nil,
|
|
345
|
+
margin: nil,
|
|
346
|
+
omit_background: nil,
|
|
347
|
+
tagged: nil,
|
|
348
|
+
outline: nil,
|
|
349
|
+
timeout: nil,
|
|
350
|
+
wait_for_fonts: nil
|
|
351
|
+
)
|
|
352
|
+
assert_not_closed
|
|
353
|
+
|
|
354
|
+
timeout_ms = timeout.nil? ? @timeout_settings.timeout : timeout
|
|
355
|
+
|
|
356
|
+
options = {
|
|
357
|
+
scale: scale,
|
|
358
|
+
display_header_footer: display_header_footer,
|
|
359
|
+
header_template: header_template,
|
|
360
|
+
footer_template: footer_template,
|
|
361
|
+
print_background: print_background,
|
|
362
|
+
landscape: landscape,
|
|
363
|
+
page_ranges: page_ranges,
|
|
364
|
+
format: format,
|
|
365
|
+
width: width,
|
|
366
|
+
height: height,
|
|
367
|
+
prefer_css_page_size: prefer_css_page_size,
|
|
368
|
+
margin: margin,
|
|
369
|
+
omit_background: omit_background,
|
|
370
|
+
tagged: tagged,
|
|
371
|
+
outline: outline,
|
|
372
|
+
wait_for_fonts: wait_for_fonts
|
|
373
|
+
}.compact
|
|
374
|
+
|
|
375
|
+
parsed = parse_pdf_options(options, "cm")
|
|
376
|
+
ranges = parsed[:page_ranges]
|
|
377
|
+
page_ranges = ranges && !ranges.empty? ? ranges.split(", ") : []
|
|
378
|
+
|
|
379
|
+
begin
|
|
380
|
+
if timeout_ms == 0
|
|
381
|
+
main_frame.isolated_realm.evaluate("() => document.fonts.ready")
|
|
382
|
+
else
|
|
383
|
+
AsyncUtils.async_timeout(timeout_ms, -> do
|
|
384
|
+
main_frame.isolated_realm.evaluate("() => document.fonts.ready")
|
|
385
|
+
end).wait
|
|
386
|
+
end
|
|
387
|
+
rescue Async::TimeoutError
|
|
388
|
+
raise TimeoutError, "Timed out after waiting #{timeout_ms}ms"
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
print_options = {
|
|
392
|
+
background: parsed[:print_background],
|
|
393
|
+
margin: parsed[:margin],
|
|
394
|
+
orientation: parsed[:landscape] ? "landscape" : "portrait",
|
|
395
|
+
page: {
|
|
396
|
+
width: parsed[:width],
|
|
397
|
+
height: parsed[:height]
|
|
398
|
+
},
|
|
399
|
+
pageRanges: page_ranges,
|
|
400
|
+
scale: parsed[:scale],
|
|
401
|
+
shrinkToFit: !parsed[:prefer_css_page_size]
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
begin
|
|
405
|
+
data = if timeout_ms == 0
|
|
406
|
+
@browsing_context.print(**print_options).wait
|
|
407
|
+
else
|
|
408
|
+
AsyncUtils.async_timeout(timeout_ms, -> do
|
|
409
|
+
@browsing_context.print(**print_options).wait
|
|
410
|
+
end).wait
|
|
411
|
+
end
|
|
412
|
+
rescue Async::TimeoutError
|
|
413
|
+
raise TimeoutError, "Timed out after waiting #{timeout_ms}ms"
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
pdf_data = Base64.decode64(data)
|
|
417
|
+
File.binwrite(path, pdf_data) if path
|
|
418
|
+
pdf_data
|
|
419
|
+
end
|
|
420
|
+
|
|
209
421
|
# Evaluate JavaScript in the page context
|
|
210
422
|
# @rbs script: String -- JavaScript code to evaluate
|
|
211
423
|
# @rbs *args: untyped -- Arguments to pass to the script
|
|
@@ -236,6 +448,24 @@ module Puppeteer
|
|
|
236
448
|
main_frame.query_selector_all(selector)
|
|
237
449
|
end
|
|
238
450
|
|
|
451
|
+
# Create a locator for a selector or function.
|
|
452
|
+
# @rbs selector: String? -- Selector to locate
|
|
453
|
+
# @rbs function: String? -- JavaScript function for function locator
|
|
454
|
+
# @rbs return: Locator -- Locator instance
|
|
455
|
+
def locator(selector = nil, function: nil)
|
|
456
|
+
assert_not_closed
|
|
457
|
+
|
|
458
|
+
if function
|
|
459
|
+
raise ArgumentError, "selector and function cannot both be set" if selector
|
|
460
|
+
|
|
461
|
+
FunctionLocator.create(self, function)
|
|
462
|
+
elsif selector
|
|
463
|
+
NodeLocator.create(self, selector)
|
|
464
|
+
else
|
|
465
|
+
raise ArgumentError, "selector or function must be provided"
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
239
469
|
# Evaluate a function on the first element matching the selector
|
|
240
470
|
# @rbs selector: String -- Selector to query
|
|
241
471
|
# @rbs page_function: String -- JavaScript function to evaluate
|
|
@@ -281,6 +511,15 @@ module Puppeteer
|
|
|
281
511
|
main_frame.hover(selector)
|
|
282
512
|
end
|
|
283
513
|
|
|
514
|
+
# Select options on a <select> element matching the selector
|
|
515
|
+
# Triggers 'change' and 'input' events once all options are selected.
|
|
516
|
+
# @rbs selector: String -- Selector for <select> element
|
|
517
|
+
# @rbs *values: String -- Option values to select
|
|
518
|
+
# @rbs return: Array[String] -- Actually selected option values
|
|
519
|
+
def select(selector, *values)
|
|
520
|
+
main_frame.select(selector, *values)
|
|
521
|
+
end
|
|
522
|
+
|
|
284
523
|
# Focus an element matching the selector
|
|
285
524
|
# @rbs selector: String -- Selector to focus
|
|
286
525
|
# @rbs return: void
|
|
@@ -307,6 +546,31 @@ module Puppeteer
|
|
|
307
546
|
@browsing_context.url
|
|
308
547
|
end
|
|
309
548
|
|
|
549
|
+
# Get the full HTML contents of the page, including the DOCTYPE.
|
|
550
|
+
# @rbs return: String -- Full HTML contents
|
|
551
|
+
def content
|
|
552
|
+
assert_not_closed
|
|
553
|
+
|
|
554
|
+
# Port of Puppeteer's Frame.content().
|
|
555
|
+
# https://github.com/puppeteer/puppeteer/blob/main/packages/puppeteer-core/src/api/Frame.ts
|
|
556
|
+
main_frame.evaluate(<<~JS)
|
|
557
|
+
() => {
|
|
558
|
+
let content = '';
|
|
559
|
+
for (const node of document.childNodes) {
|
|
560
|
+
switch (node) {
|
|
561
|
+
case document.documentElement:
|
|
562
|
+
content += document.documentElement.outerHTML;
|
|
563
|
+
break;
|
|
564
|
+
default:
|
|
565
|
+
content += new XMLSerializer().serializeToString(node);
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return content;
|
|
570
|
+
}
|
|
571
|
+
JS
|
|
572
|
+
end
|
|
573
|
+
|
|
310
574
|
# Close the page
|
|
311
575
|
# @rbs return: void
|
|
312
576
|
def close
|
|
@@ -327,6 +591,72 @@ module Puppeteer
|
|
|
327
591
|
@main_frame ||= Frame.from(self, @browsing_context)
|
|
328
592
|
end
|
|
329
593
|
|
|
594
|
+
# Reloads the page.
|
|
595
|
+
# @rbs timeout: Numeric -- Navigation timeout in ms
|
|
596
|
+
# @rbs wait_until: String | Array[String] -- When to consider navigation complete
|
|
597
|
+
# @rbs ignore_cache: bool -- Whether to ignore the browser cache
|
|
598
|
+
# @rbs return: HTTPResponse? -- Response or nil
|
|
599
|
+
def reload(timeout: 30000, wait_until: 'load', ignore_cache: false)
|
|
600
|
+
assert_not_closed
|
|
601
|
+
|
|
602
|
+
reload_options = {}
|
|
603
|
+
reload_options[:ignoreCache] = true if ignore_cache
|
|
604
|
+
|
|
605
|
+
wait_for_navigation(timeout: timeout, wait_until: wait_until) do
|
|
606
|
+
@browsing_context.reload(**reload_options).wait
|
|
607
|
+
end
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
# Enable or disable request interception.
|
|
611
|
+
# @rbs enable: bool -- Whether to enable interception
|
|
612
|
+
# @rbs return: void
|
|
613
|
+
def set_request_interception(enable)
|
|
614
|
+
assert_not_closed
|
|
615
|
+
|
|
616
|
+
@request_interception = toggle_interception(
|
|
617
|
+
["beforeRequestSent"],
|
|
618
|
+
@request_interception,
|
|
619
|
+
enable,
|
|
620
|
+
)
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
# Set extra HTTP headers for the page.
|
|
624
|
+
# @rbs headers: Hash[String, String] -- Extra headers
|
|
625
|
+
# @rbs return: void
|
|
626
|
+
def set_extra_http_headers(headers)
|
|
627
|
+
assert_not_closed
|
|
628
|
+
|
|
629
|
+
normalized = {}
|
|
630
|
+
headers.each do |key, value|
|
|
631
|
+
normalized[key.to_s] = value.to_s
|
|
632
|
+
end
|
|
633
|
+
@browsing_context.set_extra_http_headers(normalized).wait
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
# Authenticate to HTTP Basic auth challenges.
|
|
637
|
+
# @rbs credentials: Hash[Symbol, String]? -- Credentials (username/password) or nil to disable
|
|
638
|
+
# @rbs return: void
|
|
639
|
+
def authenticate(credentials)
|
|
640
|
+
assert_not_closed
|
|
641
|
+
|
|
642
|
+
@auth_interception = toggle_interception(
|
|
643
|
+
["authRequired"],
|
|
644
|
+
@auth_interception,
|
|
645
|
+
!credentials.nil?,
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
@credentials = credentials
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
# Enable or disable cache.
|
|
652
|
+
# @rbs enabled: bool -- Whether to enable cache
|
|
653
|
+
# @rbs return: void
|
|
654
|
+
def set_cache_enabled(enabled)
|
|
655
|
+
assert_not_closed
|
|
656
|
+
|
|
657
|
+
@browsing_context.set_cache_behavior(enabled ? "default" : "bypass").wait
|
|
658
|
+
end
|
|
659
|
+
|
|
330
660
|
# Get the focused frame
|
|
331
661
|
# @rbs return: Frame -- Focused frame
|
|
332
662
|
def focused_frame
|
|
@@ -430,6 +760,199 @@ module Puppeteer
|
|
|
430
760
|
main_frame.wait_for_navigation(timeout: timeout, wait_until: wait_until, &block)
|
|
431
761
|
end
|
|
432
762
|
|
|
763
|
+
# Wait for a request that matches a URL or predicate.
|
|
764
|
+
# @rbs url_or_predicate: String | ^(HTTPRequest) -> boolish -- URL or predicate
|
|
765
|
+
# @rbs timeout: Numeric? -- Timeout in ms (0 for infinite)
|
|
766
|
+
# @rbs &block: (-> void)? -- Optional block to trigger the request
|
|
767
|
+
# @rbs return: HTTPRequest
|
|
768
|
+
def wait_for_request(url_or_predicate, timeout: nil, &block)
|
|
769
|
+
assert_not_closed
|
|
770
|
+
|
|
771
|
+
timeout_ms = timeout.nil? ? @timeout_settings.timeout : timeout
|
|
772
|
+
predicate = if url_or_predicate.is_a?(Proc)
|
|
773
|
+
url_or_predicate
|
|
774
|
+
else
|
|
775
|
+
->(request) { request.url == url_or_predicate }
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
promise = Async::Promise.new
|
|
779
|
+
listener = proc do |request|
|
|
780
|
+
next unless predicate.call(request)
|
|
781
|
+
|
|
782
|
+
promise.resolve(request) unless promise.resolved?
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
begin
|
|
786
|
+
on(:request, &listener)
|
|
787
|
+
Async(&block).wait if block
|
|
788
|
+
|
|
789
|
+
if timeout_ms == 0
|
|
790
|
+
promise.wait
|
|
791
|
+
else
|
|
792
|
+
AsyncUtils.async_timeout(timeout_ms, promise).wait
|
|
793
|
+
end
|
|
794
|
+
ensure
|
|
795
|
+
off(:request, &listener)
|
|
796
|
+
end
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
# Wait for a response that matches a URL or predicate.
|
|
800
|
+
# @rbs url_or_predicate: String | ^(HTTPResponse) -> boolish -- URL or predicate
|
|
801
|
+
# @rbs timeout: Numeric? -- Timeout in ms (0 for infinite)
|
|
802
|
+
# @rbs &block: (-> void)? -- Optional block to trigger the response
|
|
803
|
+
# @rbs return: HTTPResponse
|
|
804
|
+
def wait_for_response(url_or_predicate, timeout: nil, &block)
|
|
805
|
+
assert_not_closed
|
|
806
|
+
|
|
807
|
+
timeout_ms = timeout.nil? ? @timeout_settings.timeout : timeout
|
|
808
|
+
predicate = if url_or_predicate.is_a?(Proc)
|
|
809
|
+
url_or_predicate
|
|
810
|
+
else
|
|
811
|
+
->(response) { response.url == url_or_predicate }
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
promise = Async::Promise.new
|
|
815
|
+
listener = proc do |response|
|
|
816
|
+
next unless predicate.call(response)
|
|
817
|
+
|
|
818
|
+
promise.resolve(response) unless promise.resolved?
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
begin
|
|
822
|
+
on(:response, &listener)
|
|
823
|
+
Async(&block).wait if block
|
|
824
|
+
|
|
825
|
+
if timeout_ms == 0
|
|
826
|
+
promise.wait
|
|
827
|
+
else
|
|
828
|
+
AsyncUtils.async_timeout(timeout_ms, promise).wait
|
|
829
|
+
end
|
|
830
|
+
ensure
|
|
831
|
+
off(:response, &listener)
|
|
832
|
+
end
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
# Retrieve cookies for the current page.
|
|
836
|
+
# @rbs *urls: Array[String] -- URLs to filter cookies by
|
|
837
|
+
# @rbs return: Array[Hash[String, untyped]]
|
|
838
|
+
def cookies(*urls)
|
|
839
|
+
assert_not_closed
|
|
840
|
+
|
|
841
|
+
normalized_urls = (urls.empty? ? [url] : urls).map do |cookie_url|
|
|
842
|
+
parse_cookie_url_strict(cookie_url)
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
@browsing_context.get_cookies.wait
|
|
846
|
+
.map { |cookie| CookieUtils.bidi_to_puppeteer_cookie(cookie) }
|
|
847
|
+
.select do |cookie|
|
|
848
|
+
normalized_urls.any? do |normalized_url|
|
|
849
|
+
CookieUtils.test_url_match_cookie(cookie, normalized_url)
|
|
850
|
+
end
|
|
851
|
+
end
|
|
852
|
+
end
|
|
853
|
+
|
|
854
|
+
# Set cookies for the current page.
|
|
855
|
+
# @rbs *cookies: Array[Hash[String, untyped]] -- Cookie data
|
|
856
|
+
# @rbs **cookie: untyped -- Single cookie via keyword arguments
|
|
857
|
+
# @rbs return: void
|
|
858
|
+
def set_cookie(*cookies, **cookie)
|
|
859
|
+
assert_not_closed
|
|
860
|
+
|
|
861
|
+
cookies = cookies.dup
|
|
862
|
+
cookies << cookie unless cookie.empty?
|
|
863
|
+
|
|
864
|
+
page_url = url
|
|
865
|
+
page_url_starts_with_http = page_url&.start_with?("http")
|
|
866
|
+
|
|
867
|
+
cookies.each do |raw_cookie|
|
|
868
|
+
normalized_cookie = CookieUtils.normalize_cookie_input(raw_cookie)
|
|
869
|
+
cookie_url = normalized_cookie["url"].to_s
|
|
870
|
+
if cookie_url.empty? && page_url_starts_with_http
|
|
871
|
+
cookie_url = page_url
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
if cookie_url == "about:blank"
|
|
875
|
+
raise ArgumentError, "Blank page can not have cookie \"#{normalized_cookie["name"]}\""
|
|
876
|
+
end
|
|
877
|
+
if cookie_url.start_with?("data:")
|
|
878
|
+
raise ArgumentError, "Data URL page can not have cookie \"#{normalized_cookie["name"]}\""
|
|
879
|
+
end
|
|
880
|
+
|
|
881
|
+
partition_key = normalized_cookie["partitionKey"]
|
|
882
|
+
if !partition_key.nil? && !partition_key.is_a?(String)
|
|
883
|
+
raise ArgumentError, "BiDi only allows domain partition keys"
|
|
884
|
+
end
|
|
885
|
+
|
|
886
|
+
normalized_url = parse_cookie_url(cookie_url)
|
|
887
|
+
domain = normalized_cookie["domain"] || normalized_url&.host
|
|
888
|
+
if domain.nil?
|
|
889
|
+
raise ArgumentError, "At least one of the url and domain needs to be specified"
|
|
890
|
+
end
|
|
891
|
+
|
|
892
|
+
bidi_cookie = {
|
|
893
|
+
"domain" => domain,
|
|
894
|
+
"name" => normalized_cookie["name"],
|
|
895
|
+
"value" => { "type" => "string", "value" => normalized_cookie["value"] },
|
|
896
|
+
}
|
|
897
|
+
bidi_cookie["path"] = normalized_cookie["path"] if normalized_cookie.key?("path")
|
|
898
|
+
bidi_cookie["httpOnly"] = normalized_cookie["httpOnly"] if normalized_cookie.key?("httpOnly")
|
|
899
|
+
bidi_cookie["secure"] = normalized_cookie["secure"] if normalized_cookie.key?("secure")
|
|
900
|
+
if normalized_cookie.key?("sameSite") && !normalized_cookie["sameSite"].nil?
|
|
901
|
+
bidi_cookie["sameSite"] = CookieUtils.convert_cookies_same_site_cdp_to_bidi(
|
|
902
|
+
normalized_cookie["sameSite"]
|
|
903
|
+
)
|
|
904
|
+
end
|
|
905
|
+
expiry = CookieUtils.convert_cookies_expiry_cdp_to_bidi(normalized_cookie["expires"])
|
|
906
|
+
bidi_cookie["expiry"] = expiry unless expiry.nil?
|
|
907
|
+
bidi_cookie.merge!(CookieUtils.cdp_specific_cookie_properties_from_puppeteer_to_bidi(
|
|
908
|
+
normalized_cookie,
|
|
909
|
+
"sameParty",
|
|
910
|
+
"sourceScheme",
|
|
911
|
+
"priority",
|
|
912
|
+
"url"
|
|
913
|
+
))
|
|
914
|
+
|
|
915
|
+
if partition_key
|
|
916
|
+
@browser_context.user_context.set_cookie(bidi_cookie, source_origin: partition_key).wait
|
|
917
|
+
else
|
|
918
|
+
@browsing_context.set_cookie(bidi_cookie).wait
|
|
919
|
+
end
|
|
920
|
+
end
|
|
921
|
+
end
|
|
922
|
+
|
|
923
|
+
# Delete cookies from the current page.
|
|
924
|
+
# @rbs *cookies: Array[Hash[String, untyped]] -- Cookie filters
|
|
925
|
+
# @rbs **cookie: untyped -- Single cookie filter via keyword arguments
|
|
926
|
+
# @rbs return: void
|
|
927
|
+
def delete_cookie(*cookies, **cookie)
|
|
928
|
+
assert_not_closed
|
|
929
|
+
|
|
930
|
+
cookies = cookies.dup
|
|
931
|
+
cookies << cookie unless cookie.empty?
|
|
932
|
+
|
|
933
|
+
page_url = url
|
|
934
|
+
|
|
935
|
+
tasks = cookies.map do |raw_cookie|
|
|
936
|
+
normalized_cookie = CookieUtils.normalize_cookie_input(raw_cookie)
|
|
937
|
+
cookie_url = normalized_cookie["url"] || page_url
|
|
938
|
+
normalized_url = parse_cookie_url(cookie_url.to_s)
|
|
939
|
+
domain = normalized_cookie["domain"] || normalized_url&.host
|
|
940
|
+
if domain.nil?
|
|
941
|
+
raise ArgumentError, "At least one of the url and domain needs to be specified"
|
|
942
|
+
end
|
|
943
|
+
|
|
944
|
+
filter = {
|
|
945
|
+
"domain" => domain,
|
|
946
|
+
"name" => normalized_cookie["name"],
|
|
947
|
+
}
|
|
948
|
+
filter["path"] = normalized_cookie["path"] if normalized_cookie.key?("path")
|
|
949
|
+
|
|
950
|
+
-> { @browsing_context.delete_cookie(filter).wait }
|
|
951
|
+
end
|
|
952
|
+
|
|
953
|
+
AsyncUtils.await_promise_all(*tasks) unless tasks.empty?
|
|
954
|
+
end
|
|
955
|
+
|
|
433
956
|
# Wait for a file chooser to be opened
|
|
434
957
|
# @rbs timeout: Numeric? -- Wait timeout in ms
|
|
435
958
|
# @rbs &block: (-> void)? -- Optional block to trigger file chooser
|
|
@@ -567,6 +1090,55 @@ module Puppeteer
|
|
|
567
1090
|
).wait
|
|
568
1091
|
end
|
|
569
1092
|
|
|
1093
|
+
# Set geolocation override
|
|
1094
|
+
# @rbs longitude: Numeric -- Longitude between -180 and 180
|
|
1095
|
+
# @rbs latitude: Numeric -- Latitude between -90 and 90
|
|
1096
|
+
# @rbs accuracy: Numeric? -- Non-negative accuracy value
|
|
1097
|
+
# @rbs return: void
|
|
1098
|
+
def set_geolocation(longitude:, latitude:, accuracy: nil)
|
|
1099
|
+
assert_not_closed
|
|
1100
|
+
|
|
1101
|
+
if longitude < -180 || longitude > 180
|
|
1102
|
+
raise ArgumentError, "Invalid longitude \"#{longitude}\": precondition -180 <= LONGITUDE <= 180 failed."
|
|
1103
|
+
end
|
|
1104
|
+
if latitude < -90 || latitude > 90
|
|
1105
|
+
raise ArgumentError, "Invalid latitude \"#{latitude}\": precondition -90 <= LATITUDE <= 90 failed."
|
|
1106
|
+
end
|
|
1107
|
+
accuracy_value = accuracy.nil? ? 0 : accuracy
|
|
1108
|
+
if accuracy_value < 0
|
|
1109
|
+
raise ArgumentError, "Invalid accuracy \"#{accuracy_value}\": precondition 0 <= ACCURACY failed."
|
|
1110
|
+
end
|
|
1111
|
+
|
|
1112
|
+
coordinates = {
|
|
1113
|
+
latitude: latitude,
|
|
1114
|
+
longitude: longitude
|
|
1115
|
+
}
|
|
1116
|
+
coordinates[:accuracy] = accuracy unless accuracy.nil?
|
|
1117
|
+
|
|
1118
|
+
@browsing_context.set_geolocation_override(
|
|
1119
|
+
coordinates: coordinates
|
|
1120
|
+
).wait
|
|
1121
|
+
end
|
|
1122
|
+
|
|
1123
|
+
# Set user agent
|
|
1124
|
+
# @rbs user_agent: String? -- User agent string or nil to restore original
|
|
1125
|
+
# @rbs user_agent_metadata: Hash[Symbol, untyped]? -- Not supported in BiDi-only mode
|
|
1126
|
+
# @rbs return: void
|
|
1127
|
+
def set_user_agent(user_agent, user_agent_metadata = nil)
|
|
1128
|
+
assert_not_closed
|
|
1129
|
+
|
|
1130
|
+
if user_agent.is_a?(Hash)
|
|
1131
|
+
raise UnsupportedOperationError, "options hash is not supported in BiDi-only mode"
|
|
1132
|
+
end
|
|
1133
|
+
|
|
1134
|
+
if user_agent_metadata
|
|
1135
|
+
raise UnsupportedOperationError, "userAgentMetadata is not supported in BiDi-only mode"
|
|
1136
|
+
end
|
|
1137
|
+
|
|
1138
|
+
user_agent = nil if user_agent == ""
|
|
1139
|
+
@browsing_context.set_user_agent(user_agent).wait
|
|
1140
|
+
end
|
|
1141
|
+
|
|
570
1142
|
# Get current viewport size
|
|
571
1143
|
# @rbs return: Hash[Symbol, Integer]? -- Viewport dimensions
|
|
572
1144
|
def viewport
|
|
@@ -590,8 +1162,136 @@ module Puppeteer
|
|
|
590
1162
|
@browsing_context.javascript_enabled?
|
|
591
1163
|
end
|
|
592
1164
|
|
|
1165
|
+
# Navigate backward in history
|
|
1166
|
+
# @rbs wait_until: String -- When to consider navigation complete
|
|
1167
|
+
# @rbs timeout: Numeric -- Navigation timeout in ms
|
|
1168
|
+
# @rbs return: HTTPResponse? -- Response or nil
|
|
1169
|
+
def go_back(wait_until: 'load', timeout: 30000)
|
|
1170
|
+
go(-1, wait_until: wait_until, timeout: timeout)
|
|
1171
|
+
end
|
|
1172
|
+
|
|
1173
|
+
# Navigate forward in history
|
|
1174
|
+
# @rbs wait_until: String -- When to consider navigation complete
|
|
1175
|
+
# @rbs timeout: Numeric -- Navigation timeout in ms
|
|
1176
|
+
# @rbs return: HTTPResponse? -- Response or nil
|
|
1177
|
+
def go_forward(wait_until: 'load', timeout: 30000)
|
|
1178
|
+
go(1, wait_until: wait_until, timeout: timeout)
|
|
1179
|
+
end
|
|
1180
|
+
|
|
593
1181
|
private
|
|
594
1182
|
|
|
1183
|
+
# Navigate history by delta.
|
|
1184
|
+
#
|
|
1185
|
+
# In Firefox, BFCache restores may emit `browsingContext.navigationStarted`
|
|
1186
|
+
# without firing `domContentLoaded` / `load`. We treat such navigations as
|
|
1187
|
+
# completed if we don't observe a navigation request shortly after start.
|
|
1188
|
+
# @rbs delta: Integer -- Steps to go back (negative) or forward (positive)
|
|
1189
|
+
# @rbs wait_until: String -- When to consider navigation complete
|
|
1190
|
+
# @rbs timeout: Numeric -- Navigation timeout in ms
|
|
1191
|
+
# @rbs return: HTTPResponse? -- Response or nil
|
|
1192
|
+
def go(delta, wait_until:, timeout:)
|
|
1193
|
+
assert_not_closed
|
|
1194
|
+
|
|
1195
|
+
load_event = wait_until == 'domcontentloaded' ? :dom_content_loaded : :load
|
|
1196
|
+
|
|
1197
|
+
started_promise = Async::Promise.new
|
|
1198
|
+
load_promise = Async::Promise.new
|
|
1199
|
+
navigation_request_promise = Async::Promise.new
|
|
1200
|
+
|
|
1201
|
+
navigation_id = nil
|
|
1202
|
+
navigation_url = nil
|
|
1203
|
+
|
|
1204
|
+
session = @browsing_context.user_context.browser.session
|
|
1205
|
+
|
|
1206
|
+
history_listener = proc do
|
|
1207
|
+
started_promise.resolve(:history_updated) unless started_promise.resolved?
|
|
1208
|
+
end
|
|
1209
|
+
|
|
1210
|
+
fragment_listener = proc do
|
|
1211
|
+
started_promise.resolve(:fragment_navigated) unless started_promise.resolved?
|
|
1212
|
+
end
|
|
1213
|
+
|
|
1214
|
+
nav_started_listener = proc do |info|
|
|
1215
|
+
next unless info['context'] == @browsing_context.id
|
|
1216
|
+
|
|
1217
|
+
navigation_id = info['navigation']
|
|
1218
|
+
navigation_url = info['url']
|
|
1219
|
+
started_promise.resolve(:navigation_started) unless started_promise.resolved?
|
|
1220
|
+
end
|
|
1221
|
+
|
|
1222
|
+
request_listener = proc do |data|
|
|
1223
|
+
request = data[:request]
|
|
1224
|
+
next unless navigation_id
|
|
1225
|
+
next unless request&.navigation == navigation_id
|
|
1226
|
+
|
|
1227
|
+
navigation_request_promise.resolve(nil) unless navigation_request_promise.resolved?
|
|
1228
|
+
end
|
|
1229
|
+
|
|
1230
|
+
load_listener = proc do
|
|
1231
|
+
load_promise.resolve(nil) unless load_promise.resolved?
|
|
1232
|
+
end
|
|
1233
|
+
|
|
1234
|
+
closed_listener = proc do
|
|
1235
|
+
started_promise.reject(PageClosedError.new) unless started_promise.resolved?
|
|
1236
|
+
end
|
|
1237
|
+
|
|
1238
|
+
begin
|
|
1239
|
+
session.on('browsingContext.navigationStarted', &nav_started_listener)
|
|
1240
|
+
@browsing_context.on(:history_updated, &history_listener)
|
|
1241
|
+
@browsing_context.on(:fragment_navigated, &fragment_listener)
|
|
1242
|
+
@browsing_context.on(:request, &request_listener)
|
|
1243
|
+
@browsing_context.once(load_event, &load_listener)
|
|
1244
|
+
@browsing_context.once(:closed, &closed_listener)
|
|
1245
|
+
|
|
1246
|
+
@browsing_context.traverse_history(delta).wait
|
|
1247
|
+
rescue Connection::ProtocolError => e
|
|
1248
|
+
# "History entry with delta X not found" - at history edge
|
|
1249
|
+
return nil if e.message.include?('not found')
|
|
1250
|
+
raise
|
|
1251
|
+
end
|
|
1252
|
+
|
|
1253
|
+
begin
|
|
1254
|
+
# If nothing starts soon, assume we're at the history edge.
|
|
1255
|
+
start_timeout_ms = [timeout.to_i, 500].min
|
|
1256
|
+
start_type = AsyncUtils.async_timeout(start_timeout_ms, started_promise).wait
|
|
1257
|
+
rescue Async::TimeoutError
|
|
1258
|
+
return nil
|
|
1259
|
+
end
|
|
1260
|
+
|
|
1261
|
+
case start_type
|
|
1262
|
+
when :history_updated, :fragment_navigated
|
|
1263
|
+
nil
|
|
1264
|
+
when :navigation_started
|
|
1265
|
+
# Determine BFCache restore (no navigation request) vs full navigation.
|
|
1266
|
+
begin
|
|
1267
|
+
AsyncUtils.async_timeout(200, navigation_request_promise).wait
|
|
1268
|
+
|
|
1269
|
+
begin
|
|
1270
|
+
AsyncUtils.async_timeout(timeout, load_promise).wait
|
|
1271
|
+
rescue Async::TimeoutError
|
|
1272
|
+
raise Puppeteer::Bidi::TimeoutError, "Navigation timeout of #{timeout}ms exceeded"
|
|
1273
|
+
end
|
|
1274
|
+
|
|
1275
|
+
HTTPResponse.for_bfcache(url: @browsing_context.url)
|
|
1276
|
+
rescue Async::TimeoutError
|
|
1277
|
+
# No navigation request observed: treat as BFCache restore.
|
|
1278
|
+
@browsing_context.instance_variable_set(:@url, navigation_url) if navigation_url
|
|
1279
|
+
@browsing_context.navigation&.dispose unless @browsing_context.navigation&.disposed?
|
|
1280
|
+
|
|
1281
|
+
HTTPResponse.for_bfcache(url: navigation_url || @browsing_context.url)
|
|
1282
|
+
end
|
|
1283
|
+
else
|
|
1284
|
+
nil
|
|
1285
|
+
end
|
|
1286
|
+
ensure
|
|
1287
|
+
session.off('browsingContext.navigationStarted', &nav_started_listener)
|
|
1288
|
+
@browsing_context.off(:history_updated, &history_listener)
|
|
1289
|
+
@browsing_context.off(:fragment_navigated, &fragment_listener)
|
|
1290
|
+
@browsing_context.off(:request, &request_listener)
|
|
1291
|
+
@browsing_context.off(load_event, &load_listener)
|
|
1292
|
+
@browsing_context.off(:closed, &closed_listener)
|
|
1293
|
+
end
|
|
1294
|
+
|
|
595
1295
|
# Recursively collect all frames starting from the given frame
|
|
596
1296
|
# @rbs frame: Frame -- Starting frame
|
|
597
1297
|
# @rbs return: Array[Frame] -- All frames in subtree
|
|
@@ -603,11 +1303,163 @@ module Puppeteer
|
|
|
603
1303
|
result
|
|
604
1304
|
end
|
|
605
1305
|
|
|
1306
|
+
def parse_pdf_options(options, length_unit)
|
|
1307
|
+
defaults = {
|
|
1308
|
+
scale: 1,
|
|
1309
|
+
display_header_footer: false,
|
|
1310
|
+
header_template: "",
|
|
1311
|
+
footer_template: "",
|
|
1312
|
+
print_background: false,
|
|
1313
|
+
landscape: false,
|
|
1314
|
+
page_ranges: "",
|
|
1315
|
+
prefer_css_page_size: false,
|
|
1316
|
+
omit_background: false,
|
|
1317
|
+
outline: false,
|
|
1318
|
+
tagged: true,
|
|
1319
|
+
wait_for_fonts: true
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
width = 8.5
|
|
1323
|
+
height = 11
|
|
1324
|
+
format = options[:format]
|
|
1325
|
+
if format
|
|
1326
|
+
format_key = format.to_s.downcase
|
|
1327
|
+
format_dimensions = PAPER_FORMATS[format_key]
|
|
1328
|
+
raise "Unknown paper format: #{format}" unless format_dimensions
|
|
1329
|
+
|
|
1330
|
+
dimensions = format_dimensions[length_unit]
|
|
1331
|
+
width = dimensions[:width]
|
|
1332
|
+
height = dimensions[:height]
|
|
1333
|
+
else
|
|
1334
|
+
width = convert_print_parameter_to_length_unit(options[:width], length_unit) || width
|
|
1335
|
+
height = convert_print_parameter_to_length_unit(options[:height], length_unit) || height
|
|
1336
|
+
end
|
|
1337
|
+
|
|
1338
|
+
margin_options = options[:margin]
|
|
1339
|
+
if margin_options.is_a?(Hash)
|
|
1340
|
+
margin_options = margin_options.transform_keys(&:to_sym)
|
|
1341
|
+
else
|
|
1342
|
+
margin_options = {}
|
|
1343
|
+
end
|
|
1344
|
+
margin = {
|
|
1345
|
+
top: convert_print_parameter_to_length_unit(margin_options[:top], length_unit) || 0,
|
|
1346
|
+
left: convert_print_parameter_to_length_unit(margin_options[:left], length_unit) || 0,
|
|
1347
|
+
bottom: convert_print_parameter_to_length_unit(margin_options[:bottom], length_unit) || 0,
|
|
1348
|
+
right: convert_print_parameter_to_length_unit(margin_options[:right], length_unit) || 0
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
options[:tagged] = true if options[:outline]
|
|
1352
|
+
|
|
1353
|
+
defaults.merge(options).merge(
|
|
1354
|
+
width: width,
|
|
1355
|
+
height: height,
|
|
1356
|
+
margin: margin
|
|
1357
|
+
)
|
|
1358
|
+
end
|
|
1359
|
+
|
|
1360
|
+
def convert_print_parameter_to_length_unit(parameter, length_unit)
|
|
1361
|
+
return nil if parameter.nil?
|
|
1362
|
+
|
|
1363
|
+
pixels = nil
|
|
1364
|
+
|
|
1365
|
+
if parameter.is_a?(Numeric)
|
|
1366
|
+
pixels = parameter
|
|
1367
|
+
elsif parameter.is_a?(String)
|
|
1368
|
+
text = parameter
|
|
1369
|
+
unit = text[-2, 2].to_s.downcase
|
|
1370
|
+
if UNIT_TO_PIXELS.key?(unit)
|
|
1371
|
+
value_text = text[0...-2]
|
|
1372
|
+
else
|
|
1373
|
+
unit = "px"
|
|
1374
|
+
value_text = text
|
|
1375
|
+
end
|
|
1376
|
+
value = parse_print_parameter_value(text, value_text)
|
|
1377
|
+
pixels = value * UNIT_TO_PIXELS[unit]
|
|
1378
|
+
else
|
|
1379
|
+
raise "page.pdf() Cannot handle parameter type: #{js_typeof(parameter)}"
|
|
1380
|
+
end
|
|
1381
|
+
|
|
1382
|
+
pixels / UNIT_TO_PIXELS.fetch(length_unit)
|
|
1383
|
+
end
|
|
1384
|
+
|
|
1385
|
+
def parse_print_parameter_value(text, value_text)
|
|
1386
|
+
value = Float(value_text)
|
|
1387
|
+
raise "Failed to parse parameter value: #{text}" if value.nan?
|
|
1388
|
+
|
|
1389
|
+
value
|
|
1390
|
+
rescue ArgumentError
|
|
1391
|
+
raise "Failed to parse parameter value: #{text}"
|
|
1392
|
+
end
|
|
1393
|
+
|
|
1394
|
+
def js_typeof(value)
|
|
1395
|
+
case value
|
|
1396
|
+
when String
|
|
1397
|
+
"string"
|
|
1398
|
+
when Numeric
|
|
1399
|
+
"number"
|
|
1400
|
+
when TrueClass, FalseClass
|
|
1401
|
+
"boolean"
|
|
1402
|
+
when Proc, Method
|
|
1403
|
+
"function"
|
|
1404
|
+
else
|
|
1405
|
+
"object"
|
|
1406
|
+
end
|
|
1407
|
+
end
|
|
1408
|
+
|
|
606
1409
|
# Check if this page is closed and raise error if so
|
|
607
1410
|
# @rbs return: void
|
|
608
1411
|
def assert_not_closed
|
|
609
1412
|
raise PageClosedError if closed?
|
|
610
1413
|
end
|
|
1414
|
+
|
|
1415
|
+
# @rbs cookie_url: String -- Cookie URL
|
|
1416
|
+
# @rbs return: URI::Generic? -- Parsed URL or nil
|
|
1417
|
+
def parse_cookie_url(cookie_url)
|
|
1418
|
+
return nil if cookie_url.nil? || cookie_url.empty?
|
|
1419
|
+
|
|
1420
|
+
URI.parse(cookie_url)
|
|
1421
|
+
rescue URI::InvalidURIError
|
|
1422
|
+
nil
|
|
1423
|
+
end
|
|
1424
|
+
|
|
1425
|
+
def parse_cookie_url_strict(cookie_url)
|
|
1426
|
+
normalized_url = URI.parse(cookie_url.to_s)
|
|
1427
|
+
if normalized_url.scheme.nil? ||
|
|
1428
|
+
(normalized_url.scheme.match?(/\Ahttps?\z/i) && normalized_url.host.to_s.empty?)
|
|
1429
|
+
raise ArgumentError, "Invalid URL"
|
|
1430
|
+
end
|
|
1431
|
+
normalized_url
|
|
1432
|
+
rescue URI::InvalidURIError
|
|
1433
|
+
raise ArgumentError, "Invalid URL"
|
|
1434
|
+
end
|
|
1435
|
+
|
|
1436
|
+
def toggle_interception(phases, interception, expected)
|
|
1437
|
+
if expected && interception.nil?
|
|
1438
|
+
return @browsing_context.add_intercept(phases: phases).wait
|
|
1439
|
+
end
|
|
1440
|
+
if !expected && interception
|
|
1441
|
+
@browsing_context.user_context.browser.remove_intercept(interception).wait
|
|
1442
|
+
return nil
|
|
1443
|
+
end
|
|
1444
|
+
interception
|
|
1445
|
+
end
|
|
1446
|
+
|
|
1447
|
+
# @rbs clip_x: Numeric -- Clip x coordinate
|
|
1448
|
+
# @rbs clip_y: Numeric -- Clip y coordinate
|
|
1449
|
+
# @rbs clip_width: Numeric -- Clip width
|
|
1450
|
+
# @rbs clip_height: Numeric -- Clip height
|
|
1451
|
+
# @rbs return: Array[Integer] -- Processed clip [x, y, width, height]
|
|
1452
|
+
# ref: https://github.com/puppeteer/puppeteer/commit/2275c3c0c801d42514d6de127b9b1537d20356a9
|
|
1453
|
+
def process_clip(clip_x:, clip_y:, clip_width:, clip_height:)
|
|
1454
|
+
rounded_x = clip_x.round
|
|
1455
|
+
rounded_y = clip_y.round
|
|
1456
|
+
[
|
|
1457
|
+
rounded_x,
|
|
1458
|
+
rounded_y,
|
|
1459
|
+
(clip_width + (clip_x - rounded_x)).round,
|
|
1460
|
+
(clip_height + (clip_y - rounded_y)).round,
|
|
1461
|
+
]
|
|
1462
|
+
end
|
|
611
1463
|
end
|
|
612
1464
|
end
|
|
613
1465
|
end
|