puppeteer-ruby 0.52.0 → 0.53.0
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/docs/api_coverage.md +16 -15
- data/lib/puppeteer/accessibility.rb +300 -0
- data/lib/puppeteer/browser.rb +107 -8
- data/lib/puppeteer/browser_connector.rb +42 -6
- data/lib/puppeteer/browser_context.rb +0 -1
- data/lib/puppeteer/cdp_session.rb +29 -8
- data/lib/puppeteer/chrome_target_manager.rb +145 -34
- data/lib/puppeteer/chrome_user_data_dir.rb +89 -0
- data/lib/puppeteer/connection.rb +19 -0
- data/lib/puppeteer/coverage.rb +5 -0
- data/lib/puppeteer/css_coverage.rb +4 -0
- data/lib/puppeteer/dialog.rb +5 -0
- data/lib/puppeteer/element_handle.rb +2 -3
- data/lib/puppeteer/emulation_manager.rb +70 -9
- data/lib/puppeteer/events.rb +2 -0
- data/lib/puppeteer/execution_context.rb +7 -0
- data/lib/puppeteer/extension.rb +20 -6
- data/lib/puppeteer/frame.rb +7 -3
- data/lib/puppeteer/frame_manager.rb +71 -26
- data/lib/puppeteer/http_response.rb +15 -1
- data/lib/puppeteer/isolated_world.rb +10 -4
- data/lib/puppeteer/js_coverage.rb +4 -0
- data/lib/puppeteer/keyboard.rb +6 -0
- data/lib/puppeteer/launcher/browser_options.rb +13 -1
- data/lib/puppeteer/launcher/chrome.rb +48 -4
- data/lib/puppeteer/lifecycle_watcher.rb +1 -6
- data/lib/puppeteer/locators.rb +54 -26
- data/lib/puppeteer/mouse.rb +12 -24
- data/lib/puppeteer/network_manager.rb +16 -4
- data/lib/puppeteer/page.rb +259 -33
- data/lib/puppeteer/puppeteer.rb +13 -7
- data/lib/puppeteer/screen_recorder.rb +269 -0
- data/lib/puppeteer/target.rb +7 -3
- data/lib/puppeteer/touch_screen.rb +6 -0
- data/lib/puppeteer/tracing.rb +4 -0
- data/lib/puppeteer/version.rb +2 -2
- data/lib/puppeteer/wait_task.rb +1 -2
- data/lib/puppeteer/web_mcp.rb +227 -0
- data/lib/puppeteer/web_worker.rb +135 -9
- data/lib/puppeteer.rb +11 -1
- data/puppeteer-ruby.gemspec +1 -0
- data/sig/_supplementary.rbs +33 -1
- data/sig/puppeteer/accessibility.rbs +73 -0
- data/sig/puppeteer/browser.rbs +27 -3
- data/sig/puppeteer/cdp_session.rbs +4 -0
- data/sig/puppeteer/chrome_user_data_dir.rbs +30 -0
- data/sig/puppeteer/dialog.rbs +3 -0
- data/sig/puppeteer/element_handle.rbs +1 -2
- data/sig/puppeteer/execution_context.rbs +5 -0
- data/sig/puppeteer/extension.rbs +4 -0
- data/sig/puppeteer/frame.rbs +4 -2
- data/sig/puppeteer/http_response.rbs +4 -0
- data/sig/puppeteer/keyboard.rbs +4 -0
- data/sig/puppeteer/locators.rbs +3 -4
- data/sig/puppeteer/mouse.rbs +5 -6
- data/sig/puppeteer/page.rbs +48 -4
- data/sig/puppeteer/puppeteer.rbs +5 -5
- data/sig/puppeteer/screen_recorder.rbs +48 -0
- data/sig/puppeteer/touch_screen.rbs +4 -0
- data/sig/puppeteer/web_mcp.rbs +112 -0
- data/sig/puppeteer/web_worker.rbs +39 -0
- metadata +24 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
class Puppeteer::EmulationManager
|
|
2
|
+
include Puppeteer::DebugPrint
|
|
2
3
|
using Puppeteer::DefineAsyncMethod
|
|
3
4
|
|
|
4
5
|
# @param {!Puppeteer.CDPSession} client
|
|
@@ -6,11 +7,59 @@ class Puppeteer::EmulationManager
|
|
|
6
7
|
@client = client
|
|
7
8
|
@emulating_mobile = false
|
|
8
9
|
@has_touch = false
|
|
10
|
+
@viewport = nil
|
|
11
|
+
@locale = nil
|
|
12
|
+
@locale_configured = false
|
|
13
|
+
@secondary_clients = Set.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def update_client(client)
|
|
17
|
+
@client = client
|
|
18
|
+
@secondary_clients.delete(client)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def register_speculative_session(client)
|
|
22
|
+
@secondary_clients << client
|
|
23
|
+
client.once(CDPSessionEmittedEvents::Disconnected) do
|
|
24
|
+
@secondary_clients.delete(client)
|
|
25
|
+
end
|
|
26
|
+
promises = []
|
|
27
|
+
promises.concat(viewport_promises(client, @viewport)) if @viewport
|
|
28
|
+
if @locale_configured
|
|
29
|
+
promises << client.async_send_message(
|
|
30
|
+
'Emulation.setLocaleOverride',
|
|
31
|
+
{ locale: @locale }.compact,
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
return if promises.empty?
|
|
35
|
+
|
|
36
|
+
Async do
|
|
37
|
+
Puppeteer::AsyncUtils.await_promise_all(*promises)
|
|
38
|
+
rescue => err
|
|
39
|
+
debug_puts(err)
|
|
40
|
+
end
|
|
9
41
|
end
|
|
10
42
|
|
|
11
43
|
# @param viewport [Puppeteer::Viewport]
|
|
12
44
|
# @return [true|false]
|
|
13
45
|
def emulate_viewport(viewport)
|
|
46
|
+
mobile = viewport.mobile?
|
|
47
|
+
has_touch = viewport.has_touch?
|
|
48
|
+
|
|
49
|
+
apply_viewport(@client, viewport)
|
|
50
|
+
@viewport = viewport
|
|
51
|
+
|
|
52
|
+
reload_needed = @emulating_mobile != mobile || @has_touch != has_touch
|
|
53
|
+
@emulating_mobile = mobile
|
|
54
|
+
@has_touch = has_touch
|
|
55
|
+
reload_needed
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private def apply_viewport(client, viewport)
|
|
59
|
+
Puppeteer::AsyncUtils.await_promise_all(*viewport_promises(client, viewport))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private def viewport_promises(client, viewport)
|
|
14
63
|
mobile = viewport.mobile?
|
|
15
64
|
width = viewport.width
|
|
16
65
|
height = viewport.height
|
|
@@ -23,24 +72,36 @@ class Puppeteer::EmulationManager
|
|
|
23
72
|
end
|
|
24
73
|
has_touch = viewport.has_touch?
|
|
25
74
|
|
|
26
|
-
|
|
27
|
-
|
|
75
|
+
[
|
|
76
|
+
client.async_send_message('Emulation.setDeviceMetricsOverride',
|
|
28
77
|
mobile: mobile,
|
|
29
78
|
width: width,
|
|
30
79
|
height: height,
|
|
31
80
|
deviceScaleFactor: device_scale_factor,
|
|
32
81
|
screenOrientation: screen_orientation,
|
|
33
82
|
),
|
|
34
|
-
|
|
83
|
+
client.async_send_message('Emulation.setTouchEmulationEnabled',
|
|
35
84
|
enabled: has_touch,
|
|
36
85
|
),
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
reload_needed = @emulating_mobile != mobile || @has_touch != has_touch
|
|
40
|
-
@emulating_mobile = mobile
|
|
41
|
-
@has_touch = has_touch
|
|
42
|
-
reload_needed
|
|
86
|
+
]
|
|
43
87
|
end
|
|
44
88
|
|
|
45
89
|
define_async_method :async_emulate_viewport
|
|
90
|
+
|
|
91
|
+
# @rbs locale: String? -- Locale to emulate, or nil to disable emulation
|
|
92
|
+
# @rbs return: void -- No return value
|
|
93
|
+
def emulate_locale(locale)
|
|
94
|
+
@locale = locale
|
|
95
|
+
@locale_configured = true
|
|
96
|
+
clients = [@client, *@secondary_clients]
|
|
97
|
+
promises = clients.map do |client|
|
|
98
|
+
client.async_send_message(
|
|
99
|
+
'Emulation.setLocaleOverride',
|
|
100
|
+
{ locale: locale }.compact,
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
Puppeteer::AsyncUtils.await_promise_all(*promises)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
define_async_method :async_emulate_locale
|
|
46
107
|
end
|
data/lib/puppeteer/events.rb
CHANGED
|
@@ -35,6 +35,7 @@ module CDPSessionEmittedEvents ; end
|
|
|
35
35
|
{
|
|
36
36
|
Disconnected: EventsDefinitionUtils.symbol('CDPSession.Disconnected'),
|
|
37
37
|
Ready: EventsDefinitionUtils.symbol('CDPSession.Ready'),
|
|
38
|
+
Swapped: EventsDefinitionUtils.symbol('CDPSession.Swapped'),
|
|
38
39
|
}.define_const_into(CDPSessionEmittedEvents)
|
|
39
40
|
|
|
40
41
|
# All the events a Browser may emit.
|
|
@@ -98,6 +99,7 @@ module FrameManagerEmittedEvents ; end
|
|
|
98
99
|
FrameNavigated: EventsDefinitionUtils.symbol('FrameManager.FrameNavigated'),
|
|
99
100
|
FrameDetached: EventsDefinitionUtils.symbol('FrameManager.FrameDetached'),
|
|
100
101
|
FrameSwapped: EventsDefinitionUtils.symbol('FrameManager.FrameSwapped'),
|
|
102
|
+
FrameSwappedByActivation: EventsDefinitionUtils.symbol('FrameManager.FrameSwappedByActivation'),
|
|
101
103
|
LifecycleEvent: EventsDefinitionUtils.symbol('FrameManager.LifecycleEvent'),
|
|
102
104
|
FrameNavigatedWithinDocument: EventsDefinitionUtils.symbol('FrameManager.FrameNavigatedWithinDocument'),
|
|
103
105
|
ExecutionContextCreated: EventsDefinitionUtils.symbol('FrameManager.ExecutionContextCreated'),
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
class Puppeteer::ExecutionContext
|
|
4
4
|
include Puppeteer::IfPresent
|
|
5
|
+
include Puppeteer::EventCallbackable
|
|
5
6
|
using Puppeteer::DefineAsyncMethod
|
|
6
7
|
|
|
7
8
|
EVALUATION_SCRIPT_URL = 'pprt://__puppeteer_evaluation_script__'
|
|
@@ -19,6 +20,11 @@ class Puppeteer::ExecutionContext
|
|
|
19
20
|
|
|
20
21
|
attr_reader :client, :world
|
|
21
22
|
|
|
23
|
+
# @rbs return: void -- Notify consumers that this context was destroyed
|
|
24
|
+
def dispose
|
|
25
|
+
emit_event('disposed')
|
|
26
|
+
end
|
|
27
|
+
|
|
22
28
|
# only used in IsolaatedWorld
|
|
23
29
|
private def _context_id
|
|
24
30
|
@context_id
|
|
@@ -353,6 +359,7 @@ class Puppeteer::ExecutionContext
|
|
|
353
359
|
message = error.message
|
|
354
360
|
if message.end_with?('Cannot find context with specified id') ||
|
|
355
361
|
message.end_with?('Inspected target navigated or closed')
|
|
362
|
+
|
|
356
363
|
return Puppeteer::Error.new('Execution context was destroyed, most likely because of a navigation.')
|
|
357
364
|
end
|
|
358
365
|
|
data/lib/puppeteer/extension.rb
CHANGED
|
@@ -38,11 +38,16 @@ class Puppeteer::Extension
|
|
|
38
38
|
extension_targets = @browser.targets.select do |target|
|
|
39
39
|
target.type == 'service_worker' && target.url.start_with?(extension_prefix)
|
|
40
40
|
end
|
|
41
|
-
extension_targets.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
tasks = extension_targets.map do |target|
|
|
42
|
+
proc do
|
|
43
|
+
target.worker
|
|
44
|
+
rescue => error
|
|
45
|
+
raise unless can_ignore_error?(error)
|
|
46
|
+
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
45
49
|
end
|
|
50
|
+
Puppeteer::AsyncUtils.await_promise_all(*tasks).compact
|
|
46
51
|
end
|
|
47
52
|
|
|
48
53
|
# @rbs return: Array[Puppeteer::Page] -- Extension pages
|
|
@@ -54,15 +59,24 @@ class Puppeteer::Extension
|
|
|
54
59
|
end
|
|
55
60
|
extension_targets.filter_map do |target|
|
|
56
61
|
target.as_page
|
|
57
|
-
rescue
|
|
62
|
+
rescue => error
|
|
63
|
+
raise unless can_ignore_error?(error)
|
|
64
|
+
|
|
58
65
|
nil
|
|
59
66
|
end
|
|
60
67
|
end
|
|
61
68
|
|
|
69
|
+
# @rbs error: StandardError -- Error raised while resolving an extension target
|
|
70
|
+
# @rbs return: bool -- Whether the target resolution error can be ignored
|
|
71
|
+
private def can_ignore_error?(error)
|
|
72
|
+
error.is_a?(Puppeteer::TargetCloseError) ||
|
|
73
|
+
error.message.include?('No target with given id found')
|
|
74
|
+
end
|
|
75
|
+
|
|
62
76
|
# @rbs page: Puppeteer::Page -- Target page
|
|
63
77
|
# @rbs return: void -- No return value
|
|
64
78
|
def trigger_action(page)
|
|
65
|
-
|
|
79
|
+
@browser.send(:connection).send_message('Extensions.triggerAction', {
|
|
66
80
|
id: @id,
|
|
67
81
|
targetId: page._tab_id,
|
|
68
82
|
})
|
data/lib/puppeteer/frame.rb
CHANGED
|
@@ -52,6 +52,11 @@ class Puppeteer::Frame
|
|
|
52
52
|
@frame_manager.page
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
# @rbs return: Puppeteer::Accessibility -- Accessibility tree for this frame
|
|
56
|
+
def accessibility
|
|
57
|
+
@accessibility ||= Puppeteer::Accessibility.new(self)
|
|
58
|
+
end
|
|
59
|
+
|
|
55
60
|
# @rbs return: Numeric -- Default timeout in milliseconds
|
|
56
61
|
def default_timeout
|
|
57
62
|
@frame_manager.timeout_settings.timeout
|
|
@@ -284,11 +289,10 @@ class Puppeteer::Frame
|
|
|
284
289
|
# @rbs selector: String -- CSS selector
|
|
285
290
|
# @rbs delay: Numeric? -- Delay between down and up (ms)
|
|
286
291
|
# @rbs button: String? -- Mouse button
|
|
287
|
-
# @rbs click_count: Integer? -- Deprecated: use count (click_count only sets clickCount)
|
|
288
292
|
# @rbs count: Integer? -- Number of clicks to perform
|
|
289
293
|
# @rbs return: void -- No return value
|
|
290
|
-
def click(selector, delay: nil, button: nil,
|
|
291
|
-
@puppeteer_world.click(selector, delay: delay, button: button,
|
|
294
|
+
def click(selector, delay: nil, button: nil, count: nil)
|
|
295
|
+
@puppeteer_world.click(selector, delay: delay, button: button, count: count)
|
|
292
296
|
end
|
|
293
297
|
|
|
294
298
|
define_async_method :async_click
|
|
@@ -40,6 +40,7 @@ class Puppeteer::FrameManager
|
|
|
40
40
|
@frame_tree_mutex = Mutex.new
|
|
41
41
|
|
|
42
42
|
setup_listeners(@client)
|
|
43
|
+
setup_client_disconnect_listener(@client)
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
private def setup_listeners(client)
|
|
@@ -97,6 +98,69 @@ class Puppeteer::FrameManager
|
|
|
97
98
|
|
|
98
99
|
attr_reader :client, :timeout_settings
|
|
99
100
|
|
|
101
|
+
# When the main frame is replaced by another main frame, preserve the
|
|
102
|
+
# existing Frame object while replacing its ID, client, and frame tree.
|
|
103
|
+
def swap_frame_tree(client)
|
|
104
|
+
@client = client
|
|
105
|
+
frame = @main_frame
|
|
106
|
+
if frame
|
|
107
|
+
target_id = client.target.target_id
|
|
108
|
+
@frame_naviigated_received << target_id
|
|
109
|
+
@frames.delete(frame.id)
|
|
110
|
+
frame.id = target_id
|
|
111
|
+
@frames[target_id] = frame
|
|
112
|
+
frame.send(:update_client, client)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
setup_listeners(client)
|
|
116
|
+
setup_client_disconnect_listener(client)
|
|
117
|
+
init(client.target.target_id, client)
|
|
118
|
+
@network_manager.add_client(client)
|
|
119
|
+
emit_event(FrameManagerEmittedEvents::FrameSwappedByActivation, frame) if frame
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def register_speculative_session(client)
|
|
123
|
+
@network_manager.add_client(client)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private def setup_client_disconnect_listener(client)
|
|
127
|
+
client.once(CDPSessionEmittedEvents::Disconnected) do
|
|
128
|
+
Async do
|
|
129
|
+
handle_client_disconnect(client)
|
|
130
|
+
rescue => err
|
|
131
|
+
debug_puts(err)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# A disconnected primary client can mean either that the page closed or
|
|
137
|
+
# that a prerendered target is about to replace it. Wait for one of those
|
|
138
|
+
# events instead of guessing with a timer.
|
|
139
|
+
private def handle_client_disconnect(client)
|
|
140
|
+
frame = @main_frame
|
|
141
|
+
return unless frame
|
|
142
|
+
return unless @client == client
|
|
143
|
+
|
|
144
|
+
unless @page.browser.connected? && !@page.closed?
|
|
145
|
+
remove_frame_recursively(frame)
|
|
146
|
+
return
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
frame.child_frames.each { |child| remove_frame_recursively(child) }
|
|
150
|
+
swapped = Async::Promise.new
|
|
151
|
+
swap_listener_id = add_event_listener(FrameManagerEmittedEvents::FrameSwappedByActivation) do |swapped_frame|
|
|
152
|
+
swapped.resolve(true) if swapped_frame == frame && !swapped.resolved?
|
|
153
|
+
end
|
|
154
|
+
close_listener_id = @page.add_event_listener(PageEmittedEvents::Close) do
|
|
155
|
+
swapped.resolve(false) unless swapped.resolved?
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
remove_frame_recursively(frame) unless swapped.wait
|
|
159
|
+
ensure
|
|
160
|
+
remove_event_listener(swap_listener_id) if swap_listener_id
|
|
161
|
+
@page.remove_event_listener(close_listener_id) if close_listener_id
|
|
162
|
+
end
|
|
163
|
+
|
|
100
164
|
private def init(target_id, cdp_session = nil)
|
|
101
165
|
@frames_pending_target_init[target_id] ||= Async::Promise.new
|
|
102
166
|
client = cdp_session || @client
|
|
@@ -115,7 +179,6 @@ class Puppeteer::FrameManager
|
|
|
115
179
|
client.async_send_message('Runtime.enable'),
|
|
116
180
|
@page.browser.issues_enabled? ? client.async_send_message('Audits.enable') : nil,
|
|
117
181
|
)
|
|
118
|
-
maybe_setup_block_list(client)
|
|
119
182
|
ensure_isolated_world(client, UTILITY_WORLD_NAME)
|
|
120
183
|
@network_manager.init unless cdp_session
|
|
121
184
|
rescue => err
|
|
@@ -143,6 +206,11 @@ class Puppeteer::FrameManager
|
|
|
143
206
|
# @return [Puppeteer::HTTPResponse]
|
|
144
207
|
def navigate_frame(frame, url, referer: nil, referrer_policy: nil, timeout: nil, wait_until: nil)
|
|
145
208
|
assert_no_legacy_navigation_options(wait_until: wait_until)
|
|
209
|
+
unless @page.browser.url_allowed?(url)
|
|
210
|
+
raise Puppeteer::Error.new(
|
|
211
|
+
"Navigation to #{url} is blocked by blocklist/allowlist rules",
|
|
212
|
+
)
|
|
213
|
+
end
|
|
146
214
|
|
|
147
215
|
referrer_policy ||= @network_manager.extra_http_headers['referer-policy']
|
|
148
216
|
protocol_referrer_policy = referrer_policy_to_protocol(referrer_policy)
|
|
@@ -565,6 +633,7 @@ class Puppeteer::FrameManager
|
|
|
565
633
|
context = @context_id_to_context[key]
|
|
566
634
|
return unless context
|
|
567
635
|
@context_id_to_context.delete(key)
|
|
636
|
+
context.dispose
|
|
568
637
|
context.world&.delete_context(context)
|
|
569
638
|
end
|
|
570
639
|
|
|
@@ -578,6 +647,7 @@ class Puppeteer::FrameManager
|
|
|
578
647
|
if key_session_id != session_id
|
|
579
648
|
true # keep
|
|
580
649
|
else
|
|
650
|
+
context.dispose
|
|
581
651
|
context.world&.delete_context(context)
|
|
582
652
|
false # remove
|
|
583
653
|
end
|
|
@@ -589,31 +659,6 @@ class Puppeteer::FrameManager
|
|
|
589
659
|
@context_id_to_context[key] or raise "INTERNAL ERROR: missing context with id = #{context_id}"
|
|
590
660
|
end
|
|
591
661
|
|
|
592
|
-
private def maybe_setup_block_list(client)
|
|
593
|
-
block_list = @page.browser.block_list
|
|
594
|
-
return if block_list.nil? || block_list.empty?
|
|
595
|
-
|
|
596
|
-
client.send_message('Network.enable')
|
|
597
|
-
matched_network_conditions = block_list.map do |pattern|
|
|
598
|
-
{
|
|
599
|
-
urlPattern: pattern,
|
|
600
|
-
latency: 0,
|
|
601
|
-
downloadThroughput: -1,
|
|
602
|
-
uploadThroughput: -1,
|
|
603
|
-
}
|
|
604
|
-
end
|
|
605
|
-
client.send_message('Network.emulateNetworkConditionsByRule', {
|
|
606
|
-
matchedNetworkConditions: matched_network_conditions,
|
|
607
|
-
offline: true,
|
|
608
|
-
})
|
|
609
|
-
rescue Puppeteer::Connection::ProtocolError => err
|
|
610
|
-
if err.message.include?('Method not available') || err.message.include?("wasn't found")
|
|
611
|
-
client.send_message('Network.setBlockedURLs', urls: block_list)
|
|
612
|
-
else
|
|
613
|
-
raise
|
|
614
|
-
end
|
|
615
|
-
end
|
|
616
|
-
|
|
617
662
|
# @param {!Frame} frame
|
|
618
663
|
private def remove_frame_recursively(frame)
|
|
619
664
|
frame.child_frames.each do |child|
|
|
@@ -54,7 +54,8 @@ class Puppeteer::HTTPResponse
|
|
|
54
54
|
@headers = {}
|
|
55
55
|
headers = extra_info ? extra_info['headers'] : response_payload['headers']
|
|
56
56
|
headers.each do |key, value|
|
|
57
|
-
|
|
57
|
+
header_name = key.downcase
|
|
58
|
+
@headers[header_name] = normalize_header_value(header_name, value)
|
|
58
59
|
end
|
|
59
60
|
@security_details = if_present(response_payload['securityDetails']) do |security_payload|
|
|
60
61
|
SecurityDetails.new(security_payload)
|
|
@@ -68,6 +69,19 @@ class Puppeteer::HTTPResponse
|
|
|
68
69
|
|
|
69
70
|
attr_reader :remote_address, :url, :status, :status_text, :headers, :security_details, :request, :timing
|
|
70
71
|
|
|
72
|
+
# Multiline values represent duplicate fields in CDP. RFC 9110 allows
|
|
73
|
+
# comma-combining fields except Set-Cookie, whose lines must stay separate.
|
|
74
|
+
private def normalize_header_value(name, value)
|
|
75
|
+
return value unless value.include?("\n")
|
|
76
|
+
|
|
77
|
+
separator = name == 'set-cookie' ? "\n " : ', '
|
|
78
|
+
value
|
|
79
|
+
.split("\n")
|
|
80
|
+
.map(&:strip)
|
|
81
|
+
.reject(&:empty?)
|
|
82
|
+
.join(separator)
|
|
83
|
+
end
|
|
84
|
+
|
|
71
85
|
def inspect
|
|
72
86
|
values = %i[remote_address url status status_text headers security_details request].map do |sym|
|
|
73
87
|
value = instance_variable_get(:"@#{sym}")
|
|
@@ -68,7 +68,7 @@ class Puppeteer::IsolaatedWorld
|
|
|
68
68
|
@client.on_event('Runtime.bindingCalled', &method(:handle_binding_called))
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
-
attr_reader :frame, :task_manager, :origin, :world_id
|
|
71
|
+
attr_reader :frame, :task_manager, :origin, :world_id, :context
|
|
72
72
|
|
|
73
73
|
# only used in Puppeteer::WaitTask#initialize
|
|
74
74
|
private def _bound_functions
|
|
@@ -246,6 +246,13 @@ class Puppeteer::IsolaatedWorld
|
|
|
246
246
|
# @param wait_until [String|Array<String>]
|
|
247
247
|
def set_content(html, timeout: nil, wait_until: nil)
|
|
248
248
|
option_wait_until = [wait_until || 'load'].flatten
|
|
249
|
+
unsupported_events = option_wait_until & ['networkidle0', 'networkidle2']
|
|
250
|
+
unless unsupported_events.empty?
|
|
251
|
+
raise ArgumentError.new(
|
|
252
|
+
"set_content does not support #{unsupported_events.join(', ')}; " \
|
|
253
|
+
'compose set_content with wait_for_network_idle instead.',
|
|
254
|
+
)
|
|
255
|
+
end
|
|
249
256
|
option_timeout = timeout || @timeout_settings.navigation_timeout
|
|
250
257
|
|
|
251
258
|
# We rely upon the fact that document.open() will reset frame lifecycle with "init"
|
|
@@ -398,11 +405,10 @@ class Puppeteer::IsolaatedWorld
|
|
|
398
405
|
# @param selector [String]
|
|
399
406
|
# @param delay [Number]
|
|
400
407
|
# @param button [String] "left"|"right"|"middle"
|
|
401
|
-
# @param click_count [Number] Deprecated: use count (click_count only sets clickCount)
|
|
402
408
|
# @param count [Number]
|
|
403
|
-
def click(selector, delay: nil, button: nil,
|
|
409
|
+
def click(selector, delay: nil, button: nil, count: nil)
|
|
404
410
|
handle = query_selector(selector) or raise ElementNotFoundError.new(selector)
|
|
405
|
-
handle.click(delay: delay, button: button,
|
|
411
|
+
handle.click(delay: delay, button: button, count: count)
|
|
406
412
|
handle.dispose
|
|
407
413
|
end
|
|
408
414
|
|
data/lib/puppeteer/keyboard.rb
CHANGED
|
@@ -16,6 +16,12 @@ class Puppeteer::Keyboard
|
|
|
16
16
|
|
|
17
17
|
attr_reader :modifiers
|
|
18
18
|
|
|
19
|
+
# @rbs client: Puppeteer::CDPSession -- Replacement CDP session
|
|
20
|
+
# @rbs return: void -- No return value
|
|
21
|
+
def update_client(client)
|
|
22
|
+
@client = client
|
|
23
|
+
end
|
|
24
|
+
|
|
19
25
|
# @rbs key: String -- Key name
|
|
20
26
|
# @rbs text: String? -- Text to input
|
|
21
27
|
# @rbs commands: Array[String]? -- Editing commands
|
|
@@ -38,6 +38,18 @@ module Puppeteer::Launcher
|
|
|
38
38
|
if @block_list && !@block_list.is_a?(Array)
|
|
39
39
|
raise ArgumentError.new('block_list must be an Array of URL patterns')
|
|
40
40
|
end
|
|
41
|
+
@allow_list = options[:allow_list]
|
|
42
|
+
if @allow_list && !@allow_list.is_a?(Array)
|
|
43
|
+
raise ArgumentError.new('allow_list must be an Array of URL patterns')
|
|
44
|
+
end
|
|
45
|
+
if @block_list && @allow_list
|
|
46
|
+
raise ArgumentError.new('Cannot specify both blocklist and allowlist')
|
|
47
|
+
end
|
|
48
|
+
[*@block_list, *@allow_list].each do |pattern|
|
|
49
|
+
URLPattern::URLPattern.new(pattern)
|
|
50
|
+
rescue URLPattern::Error => error
|
|
51
|
+
raise ArgumentError.new("Invalid URLPattern #{pattern.inspect}: #{error.message}")
|
|
52
|
+
end
|
|
41
53
|
|
|
42
54
|
# only for Puppeteer.connect
|
|
43
55
|
@target_filter = options[:target_filter]
|
|
@@ -51,7 +63,7 @@ module Puppeteer::Launcher
|
|
|
51
63
|
end
|
|
52
64
|
end
|
|
53
65
|
|
|
54
|
-
attr_reader :default_viewport, :slow_mo, :target_filter, :is_page_target, :network_enabled, :issues_enabled, :protocol_timeout, :block_list
|
|
66
|
+
attr_reader :default_viewport, :slow_mo, :target_filter, :is_page_target, :network_enabled, :issues_enabled, :protocol_timeout, :block_list, :allow_list
|
|
55
67
|
|
|
56
68
|
def ignore_https_errors?
|
|
57
69
|
@ignore_https_errors
|
|
@@ -3,6 +3,24 @@ require 'tmpdir'
|
|
|
3
3
|
# https://github.com/puppeteer/puppeteer/blob/main/src/node/Launcher.ts
|
|
4
4
|
module Puppeteer::Launcher
|
|
5
5
|
class Chrome
|
|
6
|
+
def self.get_features(flag, options = [])
|
|
7
|
+
prefix = flag.end_with?('=') ? flag : "#{flag}="
|
|
8
|
+
options
|
|
9
|
+
.select { |option| option.start_with?(prefix) }
|
|
10
|
+
.flat_map do |option|
|
|
11
|
+
option[(option.index('=') + 1)..]
|
|
12
|
+
.strip
|
|
13
|
+
.split(',')
|
|
14
|
+
.map(&:strip)
|
|
15
|
+
end
|
|
16
|
+
.reject(&:empty?)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.remove_matching_flags(array, flag)
|
|
20
|
+
prefix = flag.end_with?('=') ? flag : "#{flag}="
|
|
21
|
+
array.delete_if { |option| option.start_with?(prefix) }
|
|
22
|
+
end
|
|
23
|
+
|
|
6
24
|
def initialize(project_root:, preferred_revision:, is_puppeteer_core:)
|
|
7
25
|
@project_root = project_root
|
|
8
26
|
@preferred_revision = preferred_revision
|
|
@@ -89,6 +107,7 @@ module Puppeteer::Launcher
|
|
|
89
107
|
network_enabled: @browser_options.network_enabled,
|
|
90
108
|
issues_enabled: @browser_options.issues_enabled,
|
|
91
109
|
block_list: @browser_options.block_list,
|
|
110
|
+
allow_list: @browser_options.allow_list,
|
|
92
111
|
process: runner.proc,
|
|
93
112
|
close_callback: -> { runner.close },
|
|
94
113
|
target_filter_callback: nil,
|
|
@@ -120,6 +139,33 @@ module Puppeteer::Launcher
|
|
|
120
139
|
# @param options [Launcher::ChromeArgOptions]
|
|
121
140
|
def initialize(chrome_arg_options)
|
|
122
141
|
# See https://github.com/GoogleChrome/chrome-launcher/blob/main/docs/chrome-flags-for-tools.md
|
|
142
|
+
user_disabled_features = Chrome.get_features('--disable-features', chrome_arg_options.args)
|
|
143
|
+
if user_disabled_features.any?
|
|
144
|
+
Chrome.remove_matching_flags(chrome_arg_options.args, '--disable-features')
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
user_enabled_features = Chrome.get_features('--enable-features', chrome_arg_options.args)
|
|
148
|
+
if user_enabled_features.any?
|
|
149
|
+
Chrome.remove_matching_flags(chrome_arg_options.args, '--enable-features')
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
enabled_features = [
|
|
153
|
+
'NetworkServiceInProcess2',
|
|
154
|
+
*user_enabled_features,
|
|
155
|
+
].reject(&:empty?)
|
|
156
|
+
|
|
157
|
+
disabled_features = [
|
|
158
|
+
'Translate',
|
|
159
|
+
'BackForwardCache',
|
|
160
|
+
'AcceptCHFrame',
|
|
161
|
+
'MediaRouter',
|
|
162
|
+
'OptimizationHints',
|
|
163
|
+
'IPH_ReadingModePageActionLabel',
|
|
164
|
+
'ReadAnythingOmniboxChip',
|
|
165
|
+
'WebUIReloadButton',
|
|
166
|
+
*user_disabled_features,
|
|
167
|
+
].reject(&:empty?).reject { |feature| enabled_features.include?(feature) }
|
|
168
|
+
|
|
123
169
|
chrome_arguments = [
|
|
124
170
|
'--allow-pre-commit-input',
|
|
125
171
|
'--disable-background-networking',
|
|
@@ -131,8 +177,7 @@ module Puppeteer::Launcher
|
|
|
131
177
|
'--disable-component-update',
|
|
132
178
|
'--disable-default-apps',
|
|
133
179
|
'--disable-dev-shm-usage',
|
|
134
|
-
|
|
135
|
-
'--disable-features=Translate,BackForwardCache,AcceptCHFrame,MediaRouter,OptimizationHints,IPH_ReadingModePageActionLabel,ReadAnythingOmniboxChip',
|
|
180
|
+
"--disable-features=#{disabled_features.join(',')}",
|
|
136
181
|
'--disable-hang-monitor',
|
|
137
182
|
'--disable-ipc-flooding-protection',
|
|
138
183
|
'--disable-popup-blocking',
|
|
@@ -143,7 +188,7 @@ module Puppeteer::Launcher
|
|
|
143
188
|
# TODO(sadym): remove '--enable-blink-features=IdleDetection' once
|
|
144
189
|
# IdleDetection is turned on by default.
|
|
145
190
|
'--enable-blink-features=IdleDetection',
|
|
146
|
-
|
|
191
|
+
"--enable-features=#{enabled_features.join(',')}",
|
|
147
192
|
'--export-tagged-pdf',
|
|
148
193
|
'--force-color-profile=srgb',
|
|
149
194
|
'--metrics-recording-only',
|
|
@@ -179,7 +224,6 @@ module Puppeteer::Launcher
|
|
|
179
224
|
end
|
|
180
225
|
|
|
181
226
|
if chrome_arg_options.enable_extensions
|
|
182
|
-
chrome_arguments << '--enable-unsafe-extension-debugging'
|
|
183
227
|
if chrome_arg_options.enable_extensions.is_a?(Array) && !chrome_arg_options.enable_extensions.empty?
|
|
184
228
|
extension_paths = chrome_arg_options.enable_extensions.map do |path|
|
|
185
229
|
File.expand_path(path)
|
|
@@ -69,9 +69,6 @@ class Puppeteer::LifecycleWatcher
|
|
|
69
69
|
@timeout = timeout
|
|
70
70
|
|
|
71
71
|
@listener_ids = {}
|
|
72
|
-
@listener_ids['client'] = @frame_manager.client.add_event_listener(CDPSessionEmittedEvents::Disconnected) do
|
|
73
|
-
terminate(TerminatedError.new('Navigation failed because browser has disconnected!'))
|
|
74
|
-
end
|
|
75
72
|
connection = @frame_manager.client.respond_to?(:connection) ? @frame_manager.client.connection : nil
|
|
76
73
|
if connection
|
|
77
74
|
@listener_ids['connection'] = connection.add_event_listener(ConnectionEmittedEvents::Disconnected) do
|
|
@@ -85,6 +82,7 @@ class Puppeteer::LifecycleWatcher
|
|
|
85
82
|
@frame_manager.add_event_listener(FrameManagerEmittedEvents::FrameNavigatedWithinDocument, &method(:navigated_within_document)),
|
|
86
83
|
@frame_manager.add_event_listener(FrameManagerEmittedEvents::FrameNavigated, &method(:navigated)),
|
|
87
84
|
@frame_manager.add_event_listener(FrameManagerEmittedEvents::FrameSwapped, &method(:handle_frame_swapped)),
|
|
85
|
+
@frame_manager.add_event_listener(FrameManagerEmittedEvents::FrameSwappedByActivation, &method(:handle_frame_swapped)),
|
|
88
86
|
@frame_manager.add_event_listener(FrameManagerEmittedEvents::FrameDetached, &method(:handle_frame_detached)),
|
|
89
87
|
]
|
|
90
88
|
@listener_ids['network_manager'] = [
|
|
@@ -209,9 +207,6 @@ class Puppeteer::LifecycleWatcher
|
|
|
209
207
|
end
|
|
210
208
|
|
|
211
209
|
def dispose
|
|
212
|
-
if_present(@listener_ids['client']) do |id|
|
|
213
|
-
@frame_manager.client.remove_event_listener(id)
|
|
214
|
-
end
|
|
215
210
|
if_present(@listener_ids['frame_manager']) do |ids|
|
|
216
211
|
@frame_manager.remove_event_listener(*ids)
|
|
217
212
|
end
|