puppeteer-ruby 0.52.1 → 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 +1 -0
- 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/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 +6 -5
- 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/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 +4 -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 +22 -2
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
|
data/lib/puppeteer/locators.rb
CHANGED
|
@@ -197,22 +197,21 @@ class Puppeteer::Locator
|
|
|
197
197
|
|
|
198
198
|
# @rbs delay: Numeric? -- Delay between down and up (ms)
|
|
199
199
|
# @rbs button: String? -- Mouse button
|
|
200
|
-
# @rbs click_count: Integer? -- Deprecated click count
|
|
201
200
|
# @rbs count: Integer? -- Number of clicks
|
|
202
201
|
# @rbs offset: Hash[Symbol, Numeric]? -- Click offset
|
|
203
202
|
# @rbs return: void -- No return value
|
|
204
|
-
def click(delay: nil, button: nil,
|
|
203
|
+
def click(delay: nil, button: nil, count: nil, offset: nil)
|
|
205
204
|
perform_action('Locator.click',
|
|
206
205
|
conditions: [
|
|
207
206
|
method(:ensure_element_is_in_viewport_if_needed),
|
|
208
207
|
method(:wait_for_stable_bounding_box_if_needed),
|
|
209
208
|
method(:wait_for_enabled_if_needed),
|
|
210
209
|
]) do |handle, _options|
|
|
211
|
-
handle.click(delay: delay, button: button,
|
|
210
|
+
handle.click(delay: delay, button: button, count: count, offset: offset)
|
|
212
211
|
end
|
|
213
212
|
end
|
|
214
213
|
|
|
215
|
-
# @rbs value: String -- Value to fill
|
|
214
|
+
# @rbs value: String | bool -- Value to fill
|
|
216
215
|
# @rbs typing_threshold: Integer -- Minimum length to switch to direct assignment
|
|
217
216
|
# @rbs return: void -- No return value
|
|
218
217
|
def fill(value, typing_threshold: 100)
|
|
@@ -433,21 +432,28 @@ class Puppeteer::Locator
|
|
|
433
432
|
return 'typeable-input';
|
|
434
433
|
}
|
|
435
434
|
if (el instanceof HTMLInputElement) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
'
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
435
|
+
switch (el.type) {
|
|
436
|
+
case 'checkbox':
|
|
437
|
+
case 'radio':
|
|
438
|
+
return 'checkable-input';
|
|
439
|
+
case 'text':
|
|
440
|
+
case 'url':
|
|
441
|
+
case 'tel':
|
|
442
|
+
case 'search':
|
|
443
|
+
case 'password':
|
|
444
|
+
case 'number':
|
|
445
|
+
case 'email':
|
|
446
|
+
return 'typeable-input';
|
|
447
|
+
default:
|
|
448
|
+
return 'other-input';
|
|
449
449
|
}
|
|
450
|
-
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
switch (el.getAttribute('role')) {
|
|
453
|
+
case 'checkbox':
|
|
454
|
+
case 'radio':
|
|
455
|
+
case 'switch':
|
|
456
|
+
return 'checkable-input';
|
|
451
457
|
}
|
|
452
458
|
|
|
453
459
|
if (el.isContentEditable) {
|
|
@@ -459,26 +465,47 @@ class Puppeteer::Locator
|
|
|
459
465
|
JAVASCRIPT
|
|
460
466
|
|
|
461
467
|
case input_type
|
|
468
|
+
when 'checkable-input'
|
|
469
|
+
current_state = handle.evaluate(<<~JAVASCRIPT)
|
|
470
|
+
toggleEl => {
|
|
471
|
+
if (
|
|
472
|
+
toggleEl.indeterminate ||
|
|
473
|
+
toggleEl.getAttribute('aria-checked') === 'mixed'
|
|
474
|
+
) {
|
|
475
|
+
return 'mixed';
|
|
476
|
+
}
|
|
477
|
+
return (
|
|
478
|
+
toggleEl.checked ||
|
|
479
|
+
toggleEl.getAttribute('aria-checked') === 'true'
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
JAVASCRIPT
|
|
483
|
+
handle.click if current_state == 'mixed' || current_state != !!value
|
|
462
484
|
when 'select'
|
|
463
485
|
handle.select(value)
|
|
464
486
|
when 'contenteditable', 'typeable-input'
|
|
465
|
-
if value.length < typing_threshold
|
|
487
|
+
if value.is_a?(String) && value.length < typing_threshold
|
|
466
488
|
text_to_type = handle.evaluate(<<~JAVASCRIPT, value)
|
|
467
489
|
(input, newValue) => {
|
|
490
|
+
const valString = String(newValue);
|
|
468
491
|
const currentValue = input.isContentEditable
|
|
469
492
|
? input.innerText
|
|
470
493
|
: input.value;
|
|
471
494
|
|
|
495
|
+
if (currentValue === valString) {
|
|
496
|
+
return '';
|
|
497
|
+
}
|
|
498
|
+
|
|
472
499
|
if (
|
|
473
|
-
|
|
474
|
-
!
|
|
500
|
+
!valString.startsWith(currentValue) ||
|
|
501
|
+
!currentValue
|
|
475
502
|
) {
|
|
476
503
|
if (input.isContentEditable) {
|
|
477
504
|
input.innerText = '';
|
|
478
505
|
} else {
|
|
479
506
|
input.value = '';
|
|
480
507
|
}
|
|
481
|
-
return
|
|
508
|
+
return valString;
|
|
482
509
|
}
|
|
483
510
|
const originalValue = input.isContentEditable
|
|
484
511
|
? input.innerText
|
|
@@ -491,7 +518,7 @@ class Puppeteer::Locator
|
|
|
491
518
|
input.value = '';
|
|
492
519
|
input.value = originalValue;
|
|
493
520
|
}
|
|
494
|
-
return
|
|
521
|
+
return valString.substring(originalValue.length);
|
|
495
522
|
}
|
|
496
523
|
JAVASCRIPT
|
|
497
524
|
text_to_type = text_to_type.to_s
|
|
@@ -510,16 +537,17 @@ class Puppeteer::Locator
|
|
|
510
537
|
handle.focus
|
|
511
538
|
handle.evaluate(<<~JAVASCRIPT, value)
|
|
512
539
|
(input, newValue) => {
|
|
540
|
+
const valString = String(newValue);
|
|
513
541
|
const currentValue = input.isContentEditable
|
|
514
542
|
? input.innerText
|
|
515
543
|
: input.value;
|
|
516
|
-
if (currentValue ===
|
|
544
|
+
if (currentValue === valString) {
|
|
517
545
|
return;
|
|
518
546
|
}
|
|
519
547
|
if (input.isContentEditable) {
|
|
520
|
-
input.innerText =
|
|
548
|
+
input.innerText = valString;
|
|
521
549
|
} else {
|
|
522
|
-
input.value =
|
|
550
|
+
input.value = valString;
|
|
523
551
|
}
|
|
524
552
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
525
553
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
data/lib/puppeteer/mouse.rb
CHANGED
|
@@ -40,6 +40,12 @@ class Puppeteer::Mouse
|
|
|
40
40
|
@dispatch_mutex = Mutex.new
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
# @rbs client: Puppeteer::CDPSession -- Replacement CDP session
|
|
44
|
+
# @rbs return: void -- No return value
|
|
45
|
+
def update_client(client)
|
|
46
|
+
@client = client
|
|
47
|
+
end
|
|
48
|
+
|
|
43
49
|
# @rbs return: void -- No return value
|
|
44
50
|
def reset
|
|
45
51
|
[
|
|
@@ -102,48 +108,30 @@ class Puppeteer::Mouse
|
|
|
102
108
|
# @rbs y: Numeric -- Y coordinate
|
|
103
109
|
# @rbs delay: Numeric? -- Delay between down and up (ms)
|
|
104
110
|
# @rbs button: String? -- Mouse button
|
|
105
|
-
# @rbs click_count: Integer? -- Deprecated: use count (click_count only sets clickCount)
|
|
106
111
|
# @rbs count: Integer? -- Number of click repetitions
|
|
107
112
|
# @rbs return: void -- No return value
|
|
108
|
-
def click(x, y, delay: nil, button: nil,
|
|
109
|
-
warn_deprecated_click_count if !click_count.nil?
|
|
113
|
+
def click(x, y, delay: nil, button: nil, count: nil)
|
|
110
114
|
count ||= 1
|
|
111
|
-
click_count ||= count
|
|
112
115
|
if count < 1
|
|
113
116
|
raise Puppeteer::Error.new('Click must occur a positive number of times.')
|
|
114
117
|
end
|
|
115
118
|
# Serialize click sequences to keep event ordering stable under thread-based concurrency.
|
|
116
119
|
@dispatch_mutex.synchronize do
|
|
117
120
|
move(x, y)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
up(button: button, click_count: i)
|
|
122
|
-
end
|
|
121
|
+
1.upto(count - 1) do |i|
|
|
122
|
+
down(button: button, click_count: i)
|
|
123
|
+
up(button: button, click_count: i)
|
|
123
124
|
end
|
|
124
|
-
down(button: button, click_count:
|
|
125
|
+
down(button: button, click_count: count)
|
|
125
126
|
if !delay.nil?
|
|
126
127
|
Puppeteer::AsyncUtils.sleep_seconds(delay / 1000.0)
|
|
127
128
|
end
|
|
128
|
-
up(button: button, click_count:
|
|
129
|
+
up(button: button, click_count: count)
|
|
129
130
|
end
|
|
130
131
|
end
|
|
131
132
|
|
|
132
133
|
define_async_method :async_click
|
|
133
134
|
|
|
134
|
-
private def warn_deprecated_click_count
|
|
135
|
-
return if self.class.deprecated_click_count_warned
|
|
136
|
-
|
|
137
|
-
self.class.deprecated_click_count_warned = true
|
|
138
|
-
warn('DEPRECATED: `click_count` is deprecated; use `count` instead.')
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
class << self
|
|
142
|
-
attr_accessor :deprecated_click_count_warned
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
self.deprecated_click_count_warned = false
|
|
146
|
-
|
|
147
135
|
# @rbs button: String? -- Mouse button
|
|
148
136
|
# @rbs click_count: Integer? -- Click count to report
|
|
149
137
|
# @rbs return: void -- No return value
|