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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/docs/api_coverage.md +16 -15
  3. data/lib/puppeteer/accessibility.rb +300 -0
  4. data/lib/puppeteer/browser.rb +107 -8
  5. data/lib/puppeteer/browser_connector.rb +42 -6
  6. data/lib/puppeteer/browser_context.rb +0 -1
  7. data/lib/puppeteer/cdp_session.rb +29 -8
  8. data/lib/puppeteer/chrome_target_manager.rb +145 -34
  9. data/lib/puppeteer/chrome_user_data_dir.rb +89 -0
  10. data/lib/puppeteer/connection.rb +19 -0
  11. data/lib/puppeteer/coverage.rb +5 -0
  12. data/lib/puppeteer/css_coverage.rb +4 -0
  13. data/lib/puppeteer/dialog.rb +5 -0
  14. data/lib/puppeteer/element_handle.rb +2 -3
  15. data/lib/puppeteer/emulation_manager.rb +70 -9
  16. data/lib/puppeteer/events.rb +2 -0
  17. data/lib/puppeteer/execution_context.rb +7 -0
  18. data/lib/puppeteer/extension.rb +20 -6
  19. data/lib/puppeteer/frame.rb +7 -3
  20. data/lib/puppeteer/frame_manager.rb +71 -26
  21. data/lib/puppeteer/http_response.rb +15 -1
  22. data/lib/puppeteer/isolated_world.rb +10 -4
  23. data/lib/puppeteer/js_coverage.rb +4 -0
  24. data/lib/puppeteer/keyboard.rb +6 -0
  25. data/lib/puppeteer/launcher/browser_options.rb +13 -1
  26. data/lib/puppeteer/launcher/chrome.rb +48 -4
  27. data/lib/puppeteer/lifecycle_watcher.rb +1 -6
  28. data/lib/puppeteer/locators.rb +54 -26
  29. data/lib/puppeteer/mouse.rb +12 -24
  30. data/lib/puppeteer/network_manager.rb +16 -4
  31. data/lib/puppeteer/page.rb +259 -33
  32. data/lib/puppeteer/puppeteer.rb +13 -7
  33. data/lib/puppeteer/screen_recorder.rb +269 -0
  34. data/lib/puppeteer/target.rb +7 -3
  35. data/lib/puppeteer/touch_screen.rb +6 -0
  36. data/lib/puppeteer/tracing.rb +4 -0
  37. data/lib/puppeteer/version.rb +2 -2
  38. data/lib/puppeteer/wait_task.rb +1 -2
  39. data/lib/puppeteer/web_mcp.rb +227 -0
  40. data/lib/puppeteer/web_worker.rb +135 -9
  41. data/lib/puppeteer.rb +11 -1
  42. data/puppeteer-ruby.gemspec +1 -0
  43. data/sig/_supplementary.rbs +33 -1
  44. data/sig/puppeteer/accessibility.rbs +73 -0
  45. data/sig/puppeteer/browser.rbs +27 -3
  46. data/sig/puppeteer/cdp_session.rbs +4 -0
  47. data/sig/puppeteer/chrome_user_data_dir.rbs +30 -0
  48. data/sig/puppeteer/dialog.rbs +3 -0
  49. data/sig/puppeteer/element_handle.rbs +1 -2
  50. data/sig/puppeteer/execution_context.rbs +5 -0
  51. data/sig/puppeteer/extension.rbs +4 -0
  52. data/sig/puppeteer/frame.rbs +4 -2
  53. data/sig/puppeteer/http_response.rbs +4 -0
  54. data/sig/puppeteer/keyboard.rbs +4 -0
  55. data/sig/puppeteer/locators.rbs +3 -4
  56. data/sig/puppeteer/mouse.rbs +5 -6
  57. data/sig/puppeteer/page.rbs +48 -4
  58. data/sig/puppeteer/puppeteer.rbs +5 -5
  59. data/sig/puppeteer/screen_recorder.rbs +48 -0
  60. data/sig/puppeteer/touch_screen.rbs +4 -0
  61. data/sig/puppeteer/web_mcp.rbs +112 -0
  62. data/sig/puppeteer/web_worker.rbs +39 -0
  63. 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
- Puppeteer::AsyncUtils.await_promise_all(
27
- @client.async_send_message('Emulation.setDeviceMetricsOverride',
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
- @client.async_send_message('Emulation.setTouchEmulationEnabled',
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
@@ -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
 
@@ -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.filter_map do |target|
42
- target.worker
43
- rescue
44
- nil
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
- page.browser.send(:connection).send_message('Extensions.triggerAction', {
79
+ @browser.send(:connection).send_message('Extensions.triggerAction', {
66
80
  id: @id,
67
81
  targetId: page._tab_id,
68
82
  })
@@ -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, click_count: nil, count: nil)
291
- @puppeteer_world.click(selector, delay: delay, button: button, click_count: click_count, count: count)
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
- @headers[key.downcase] = value
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, click_count: nil, count: 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, click_count: click_count, count: count)
411
+ handle.click(delay: delay, button: button, count: count)
406
412
  handle.dispose
407
413
  end
408
414
 
@@ -29,6 +29,10 @@ class Puppeteer::JSCoverage
29
29
  @script_parsed_tasks = []
30
30
  end
31
31
 
32
+ def update_client(client)
33
+ @client = client
34
+ end
35
+
32
36
  def start(
33
37
  reset_on_navigation: nil,
34
38
  report_anonymous_scripts: nil,
@@ -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
- # AcceptCHFrame disabled because of crbug.com/1348106.
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
- '--enable-features=NetworkServiceInProcess2',
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