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
  require_relative './browser'
2
+ require_relative './chrome_user_data_dir'
2
3
  require_relative './launcher/browser_options'
3
4
 
4
5
  class Puppeteer::BrowserConnector
@@ -7,6 +8,7 @@ class Puppeteer::BrowserConnector
7
8
  @browser_ws_endpoint = options[:browser_ws_endpoint]
8
9
  @browser_url = options[:browser_url]
9
10
  @transport = options[:transport]
11
+ @channel = options[:channel]
10
12
  end
11
13
 
12
14
  # @return [Puppeteer::Browser]
@@ -30,6 +32,7 @@ class Puppeteer::BrowserConnector
30
32
  network_enabled: @browser_options.network_enabled,
31
33
  issues_enabled: @browser_options.issues_enabled,
32
34
  block_list: @browser_options.block_list,
35
+ allow_list: @browser_options.allow_list,
33
36
  process: nil,
34
37
  close_callback: -> { connection.send_message('Browser.close') },
35
38
  target_filter_callback: @browser_options.target_filter,
@@ -38,16 +41,24 @@ class Puppeteer::BrowserConnector
38
41
  end
39
42
 
40
43
  private def connection
41
- @connection ||=
42
- if @browser_ws_endpoint && @browser_url.nil? && @transport.nil?
44
+ @connection ||= begin
45
+ connection_options = [@browser_ws_endpoint, @browser_url, @transport, @channel]
46
+ unless connection_options.count { |option| !!option } == 1
47
+ raise ArgumentError.new('Exactly one of browserWSEndpoint, browserURL, transport or channel must be passed to puppeteer.connect')
48
+ end
49
+
50
+ if @transport
51
+ connect_with_transport(@transport)
52
+ elsif @browser_ws_endpoint
43
53
  connect_with_browser_ws_endpoint(@browser_ws_endpoint)
44
- elsif @browser_ws_endpoint.nil? && @browser_url && @transport.nil?
54
+ elsif @browser_url
45
55
  connect_with_browser_url(@browser_url)
46
- elsif @browser_ws_endpoint.nil? && @browser_url.nil? && @transport
47
- connect_with_transport(@transport)
56
+ elsif @channel
57
+ connect_with_channel(@channel)
48
58
  else
49
- raise ArgumentError.new("Exactly one of browserWSEndpoint, browserURL or transport must be passed to puppeteer.connect")
59
+ raise ArgumentError.new('Invalid connection options')
50
60
  end
61
+ end
51
62
  end
52
63
 
53
64
  # @return [Puppeteer::Connection]
@@ -72,6 +83,31 @@ class Puppeteer::BrowserConnector
72
83
  connect_with_browser_ws_endpoint(connection_url)
73
84
  end
74
85
 
86
+ # @return [Puppeteer::Connection]
87
+ private def connect_with_channel(channel)
88
+ port_path = File.join(
89
+ Puppeteer::ChromeUserDataDir.resolve_default(channel),
90
+ 'DevToolsActivePort',
91
+ )
92
+
93
+ begin
94
+ file_content = File.read(port_path, mode: 'r:ASCII')
95
+ raw_port, raw_path = file_content.lines.map(&:strip).reject(&:empty?)
96
+ unless raw_port && raw_path
97
+ raise Puppeteer::Error.new("Invalid DevToolsActivePort '#{file_content}' found")
98
+ end
99
+
100
+ port = raw_port.to_i
101
+ if port <= 0 || port > 65_535
102
+ raise Puppeteer::Error.new("Invalid port '#{raw_port}' found")
103
+ end
104
+
105
+ connect_with_browser_ws_endpoint("ws://localhost:#{port}#{raw_path}")
106
+ rescue StandardError
107
+ raise Puppeteer::Error.new("Could not find DevToolsActivePort for #{channel} at #{port_path}")
108
+ end
109
+ end
110
+
75
111
  # @return [Puppeteer::Connection]
76
112
  private def connect_with_transport(transport)
77
113
  Puppeteer::Connection.new(
@@ -241,7 +241,6 @@ class Puppeteer::BrowserContext
241
241
  if partition_key
242
242
  normalized['partitionKey'] = convert_partition_key_from_cdp(partition_key)
243
243
  end
244
- normalized['sameParty'] = cookie['sameParty'] || false
245
244
  normalized
246
245
  end
247
246
  end
@@ -66,9 +66,14 @@ class Puppeteer::CDPSession
66
66
  # @rbs return: Async::Promise[Hash[String, untyped]] -- Async CDP response
67
67
  def async_send_message(method, params = {})
68
68
  if !@connection
69
- raise Error.new("Protocol error (#{method}): Session closed. Most likely the #{@target_type} has been closed.")
69
+ raise Puppeteer::TargetCloseError.new(
70
+ method: method,
71
+ error_message: "Session closed. Most likely the #{@target_type} has been closed.",
72
+ )
70
73
  end
71
74
 
75
+ @connection.ensure_command_allowed!(method)
76
+
72
77
  promise = Async::Promise.new
73
78
 
74
79
  @connection.generate_id do |id|
@@ -81,6 +86,12 @@ class Puppeteer::CDPSession
81
86
  promise
82
87
  end
83
88
 
89
+ # @rbs id: Integer -- CDP command id
90
+ # @rbs return: bool -- True when the session owns the callback
91
+ def callback?(id)
92
+ @callbacks_mutex.synchronize { @callbacks.key?(id) }
93
+ end
94
+
84
95
  # @rbs message: Hash[String, untyped] -- Raw CDP message
85
96
  # @rbs return: void -- No return value
86
97
  def handle_message(message)
@@ -97,11 +108,21 @@ class Puppeteer::CDPSession
97
108
 
98
109
  private def callback_with_message(callback, message)
99
110
  if message['error']
100
- callback.reject(
101
- Puppeteer::Connection::ProtocolError.new(
102
- method: callback.method,
103
- error_message: message['error']['message'],
104
- error_data: message['error']['data']))
111
+ if message['error']['message'].include?('Session with given id not found')
112
+ handle_closed
113
+ callback.reject(
114
+ Puppeteer::TargetCloseError.new(
115
+ method: callback.method,
116
+ error_message: 'Session with given id not found.',
117
+ ),
118
+ )
119
+ else
120
+ callback.reject(
121
+ Puppeteer::Connection::ProtocolError.new(
122
+ method: callback.method,
123
+ error_message: message['error']['message'],
124
+ error_data: message['error']['data']))
125
+ end
105
126
  else
106
127
  callback.resolve(message['result'])
107
128
  end
@@ -122,9 +143,9 @@ class Puppeteer::CDPSession
122
143
  end
123
144
  callbacks.each do |callback|
124
145
  callback.reject(
125
- Puppeteer::Connection::ProtocolError.new(
146
+ Puppeteer::TargetCloseError.new(
126
147
  method: callback.method,
127
- error_message: 'Target Closed.'))
148
+ error_message: 'Target closed'))
128
149
  end
129
150
  @ready_promise.reject(Error.new("Session closed")) unless @ready_promise.resolved?
130
151
  @connection = nil
@@ -2,7 +2,7 @@ class Puppeteer::ChromeTargetManager
2
2
  include Puppeteer::DebugPrint
3
3
  include Puppeteer::EventCallbackable
4
4
 
5
- def initialize(connection:, target_factory:, target_filter_callback:, block_list: nil)
5
+ def initialize(connection:, target_factory:, target_filter_callback:, block_list: nil, allow_list: nil)
6
6
  @discovered_targets_by_target_id = {}
7
7
  @attached_targets_by_target_id = {}
8
8
  @attached_targets_by_session_id = {}
@@ -13,10 +13,17 @@ class Puppeteer::ChromeTargetManager
13
13
  @connection = connection
14
14
  @target_filter_callback = target_filter_callback
15
15
  @target_factory = target_factory
16
- @block_list = block_list
17
- if @block_list && !@block_list.is_a?(Array)
16
+ if block_list && allow_list
17
+ raise ArgumentError.new('Cannot specify both blocklist and allowlist')
18
+ end
19
+ if block_list && !block_list.is_a?(Array)
18
20
  raise ArgumentError.new('block_list must be an Array of URL patterns')
19
21
  end
22
+ if allow_list && !allow_list.is_a?(Array)
23
+ raise ArgumentError.new('allow_list must be an Array of URL patterns')
24
+ end
25
+ @block_list = map_patterns(block_list)
26
+ @allow_list = map_patterns(allow_list)
20
27
  @target_interceptors = {}
21
28
  @initialize_promise = Async::Promise.new
22
29
  @initial_attach_done = false
@@ -44,10 +51,7 @@ class Puppeteer::ChromeTargetManager
44
51
  Async do
45
52
  @connection.async_send_message('Target.setDiscoverTargets', {
46
53
  discover: true,
47
- filter: [
48
- { type: 'tab', exclude: true },
49
- {},
50
- ],
54
+ filter: [{}],
51
55
  }).wait
52
56
  store_existing_targets_for_init
53
57
  rescue => err
@@ -57,7 +61,7 @@ class Puppeteer::ChromeTargetManager
57
61
 
58
62
  private def store_existing_targets_for_init
59
63
  @discovered_targets_by_target_id.each do |target_id, target_info|
60
- if @target_filter_callback.call(target_info) && target_info.type != 'browser' && url_allowed?(target_info.url)
64
+ if @target_filter_callback.call(target_info) && target_info.type == 'tab' && url_allowed?(target_info.url)
61
65
  @target_ids_for_init << target_id
62
66
  end
63
67
  end
@@ -68,6 +72,10 @@ class Puppeteer::ChromeTargetManager
68
72
  waitForDebuggerOnStart: true,
69
73
  flatten: true,
70
74
  autoAttach: true,
75
+ filter: [
76
+ { type: 'page', exclude: true },
77
+ {},
78
+ ],
71
79
  })
72
80
  @initial_attach_done = true
73
81
  finish_initialization_if_ready
@@ -83,6 +91,19 @@ class Puppeteer::ChromeTargetManager
83
91
  @attached_targets_by_target_id
84
92
  end
85
93
 
94
+ def remove_extension_service_workers(extension_id)
95
+ extension_url = "chrome-extension://#{extension_id}/"
96
+ target_ids = @discovered_targets_by_target_id.filter_map do |target_id, target_info|
97
+ if target_info.type == 'service_worker' && target_info.url.start_with?(extension_url)
98
+ target_id
99
+ end
100
+ end
101
+ target_ids.each do |target_id|
102
+ @ignored_targets << target_id
103
+ handle_target_destroyed('targetId' => target_id)
104
+ end
105
+ end
106
+
86
107
  def wait_for_service_worker_detach(target_id)
87
108
  promise = @service_worker_detach_promises[target_id]
88
109
  return unless promise
@@ -171,6 +192,10 @@ class Puppeteer::ChromeTargetManager
171
192
  return
172
193
  end
173
194
  original_target = @attached_targets_by_target_id[target_info.target_id]
195
+ if original_target&.target_info&.subtype && !target_info.subtype
196
+ session = original_target.session
197
+ session&.parent_session&.emit_event(CDPSessionEmittedEvents::Swapped, session)
198
+ end
174
199
  emit_event(TargetManagerEmittedEvents::TargetChanged, original_target, target_info) if original_target
175
200
  end
176
201
 
@@ -189,7 +214,7 @@ class Puppeteer::ChromeTargetManager
189
214
  begin
190
215
  Puppeteer::AsyncUtils.await(session.async_send_message('Runtime.runIfWaitingForDebugger'))
191
216
  rescue => err
192
- Logger.new($stderr).warn(err)
217
+ debug_puts(err)
193
218
  end
194
219
 
195
220
  # We don't use `session.detach()` because that dispatches all commands on
@@ -199,7 +224,7 @@ class Puppeteer::ChromeTargetManager
199
224
  sessionId: session.id,
200
225
  }))
201
226
  rescue => err
202
- Logger.new($stderr).warn(err)
227
+ debug_puts(err)
203
228
  ensure
204
229
  if detached_promise && !detached_promise.resolved?
205
230
  detached_promise.resolve(true)
@@ -208,7 +233,16 @@ class Puppeteer::ChromeTargetManager
208
233
  end
209
234
  }
210
235
 
211
- return unless @connection.auto_attached?(target_info.target_id)
236
+ unless @connection.auto_attached?(target_info.target_id)
237
+ Async do
238
+ maybe_setup_network_conditions(session, target_info)
239
+ rescue => err
240
+ debug_puts(err)
241
+ ensure
242
+ session.mark_ready
243
+ end
244
+ return
245
+ end
212
246
 
213
247
  if !@initial_attach_done && !url_allowed?(target_info.url)
214
248
  finish_initialization_if_ready(target_info.target_id)
@@ -225,6 +259,23 @@ class Puppeteer::ChromeTargetManager
225
259
  # CDP.
226
260
  if target_info.type == 'service_worker' && @connection.auto_attached?(target_info.target_id)
227
261
  finish_initialization_if_ready(target_info.target_id)
262
+ unless url_allowed?(target_info.url)
263
+ Async do
264
+ setup_task = Async do
265
+ maybe_setup_network_conditions(session, target_info)
266
+ rescue => err
267
+ debug_puts(err)
268
+ end
269
+ run_promise = session.async_send_message('Runtime.runIfWaitingForDebugger')
270
+ Puppeteer::AsyncUtils.await(run_promise)
271
+ Puppeteer::AsyncUtils.await(setup_task)
272
+ rescue => err
273
+ debug_puts(err)
274
+ ensure
275
+ session.mark_ready
276
+ end
277
+ return
278
+ end
228
279
  @service_worker_detach_promises[target_info.target_id] ||= Async::Promise.new
229
280
  silent_detach.call(@service_worker_detach_promises[target_info.target_id])
230
281
  return if @attached_targets_by_target_id.has_key?(target_info.target_id)
@@ -238,7 +289,8 @@ class Puppeteer::ChromeTargetManager
238
289
 
239
290
  unless @target_filter_callback.call(target_info)
240
291
  @ignored_targets << target_info.target_id
241
- finish_initialization_if_ready(target_info.target_id)
292
+ parent_target = parent_session.is_a?(Puppeteer::CDPSession) ? parent_session.target : nil
293
+ finish_initialization_if_ready(parent_target.target_id) if parent_target&.raw_type == 'tab'
242
294
  silent_detach.call
243
295
 
244
296
  return
@@ -267,25 +319,39 @@ class Puppeteer::ChromeTargetManager
267
319
  end
268
320
  end
269
321
 
270
- @target_ids_for_init.delete(target.target_id)
271
322
  unless is_existing_target
272
323
  Async do
273
324
  Puppeteer::AsyncUtils.future_with_logging { emit_event(TargetManagerEmittedEvents::TargetAvailable, target) }.call
274
325
  end
275
326
  end
327
+ parent_target = parent_session.is_a?(Puppeteer::CDPSession) ? parent_session.target : nil
328
+ finish_initialization_if_ready(parent_target.target_id) if parent_target&.raw_type == 'tab'
276
329
  finish_initialization_if_ready
277
330
  parent_session.emit_event(CDPSessionEmittedEvents::Ready, session)
278
331
 
279
332
  Async do
280
- Puppeteer::AsyncUtils.await(session.async_send_message('Target.setAutoAttach', {
281
- waitForDebuggerOnStart: true,
282
- flatten: true,
283
- autoAttach: true,
284
- }))
285
- maybe_setup_network_conditions(session)
286
- Puppeteer::AsyncUtils.await(session.async_send_message('Runtime.runIfWaitingForDebugger'))
333
+ setup_task = Async do
334
+ maybe_setup_network_conditions(session, target_info)
335
+ rescue => err
336
+ debug_puts(err)
337
+ end
338
+ promises = [
339
+ session.async_send_message('Target.setAutoAttach', {
340
+ waitForDebuggerOnStart: true,
341
+ flatten: true,
342
+ autoAttach: true,
343
+ filter: [{}],
344
+ }),
345
+ session.async_send_message('Runtime.runIfWaitingForDebugger'),
346
+ setup_task,
347
+ ]
348
+ promises.each do |promise|
349
+ Puppeteer::AsyncUtils.await(promise)
350
+ rescue => err
351
+ debug_puts(err)
352
+ end
287
353
  rescue => err
288
- Logger.new($stderr).warn(err)
354
+ debug_puts(err)
289
355
  ensure
290
356
  session.mark_ready
291
357
  end
@@ -307,32 +373,77 @@ class Puppeteer::ChromeTargetManager
307
373
  end
308
374
 
309
375
  private def url_allowed?(url)
310
- return true if @block_list.nil? || @block_list.empty?
376
+ block_list = @block_list || []
377
+ allow_list = @allow_list || []
378
+ return true if block_list.empty? && allow_list.empty?
311
379
  return true if url.nil? || url.empty? || url == 'about:blank'
312
380
 
313
- @block_list.none? do |pattern|
314
- File.fnmatch?(pattern, url, File::FNM_EXTGLOB)
315
- rescue ArgumentError
316
- false
381
+ blocked = block_list.any? { |item| item[:pattern].test?(url) }
382
+ return false if blocked
383
+
384
+ return true if allow_list.empty?
385
+
386
+ allow_list.any? { |item| item[:pattern].test?(url) }
387
+ end
388
+ public :url_allowed?
389
+
390
+ private def map_patterns(rules)
391
+ return nil if rules.nil?
392
+
393
+ rules.map do |rule|
394
+ {
395
+ pattern: URLPattern::URLPattern.new(rule),
396
+ rule: rule,
397
+ }
317
398
  end
318
399
  end
319
400
 
320
- private def maybe_setup_network_conditions(session)
321
- return if @block_list.nil? || @block_list.empty?
401
+ private def maybe_setup_network_conditions(session, target_info)
402
+ block_list = @block_list || []
403
+ allow_list = @allow_list || []
404
+ return if block_list.empty? && allow_list.empty?
322
405
 
323
- matched_network_conditions = @block_list.map do |pattern|
406
+ matched_network_conditions = block_list.map do |item|
324
407
  {
325
- urlPattern: pattern,
408
+ urlPattern: item[:rule],
409
+ offline: true,
326
410
  latency: 0,
327
411
  downloadThroughput: -1,
328
412
  uploadThroughput: -1,
329
413
  }
330
414
  end
331
- Puppeteer::AsyncUtils.await(session.async_send_message('Network.enable'))
332
- Puppeteer::AsyncUtils.await(session.async_send_message('Network.emulateNetworkConditionsByRule', {
415
+ allow_list.each do |item|
416
+ matched_network_conditions << {
417
+ urlPattern: item[:rule],
418
+ offline: false,
419
+ latency: 0,
420
+ downloadThroughput: -1,
421
+ uploadThroughput: -1,
422
+ }
423
+ end
424
+ unless allow_list.empty?
425
+ matched_network_conditions << {
426
+ urlPattern: '',
427
+ offline: true,
428
+ latency: 0,
429
+ downloadThroughput: -1,
430
+ uploadThroughput: -1,
431
+ }
432
+ end
433
+
434
+ promises = []
435
+ worker_types = ['worker', 'service_worker', 'shared_worker']
436
+ promises << session.async_send_message('Network.enable') if worker_types.include?(target_info.type)
437
+ params = {
438
+ offline: block_list.empty? ? nil : true,
333
439
  matchedNetworkConditions: matched_network_conditions,
334
- offline: true,
335
- }))
440
+ }.compact
441
+ promises << session.async_send_message('Network.emulateNetworkConditionsByRule', params)
442
+ promises.each do |promise|
443
+ Puppeteer::AsyncUtils.await(promise)
444
+ rescue => err
445
+ debug_puts(err)
446
+ end
336
447
  rescue => err
337
448
  message = err.message.to_s.downcase
338
449
  return if message.include?('target closed') || message.include?('session closed') || message.include?('not found')
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require_relative './env'
5
+
6
+ module Puppeteer::ChromeUserDataDir
7
+ CHANNELS = %w[chrome chrome-beta chrome-canary chrome-dev].freeze #: Array[String]
8
+
9
+ MAC_DIR_NAMES = {
10
+ 'chrome' => 'Chrome',
11
+ 'chrome-beta' => 'Chrome Beta',
12
+ 'chrome-canary' => 'Chrome Canary',
13
+ 'chrome-dev' => 'Chrome Dev',
14
+ }.freeze #: Hash[String, String]
15
+
16
+ LINUX_DIR_NAMES = {
17
+ 'chrome' => 'google-chrome',
18
+ 'chrome-beta' => 'google-chrome-beta',
19
+ 'chrome-canary' => 'google-chrome-canary',
20
+ 'chrome-dev' => 'google-chrome-unstable',
21
+ }.freeze #: Hash[String, String]
22
+
23
+ WINDOWS_DIR_NAMES = {
24
+ 'chrome' => ['Google', 'Chrome', 'User Data'],
25
+ 'chrome-beta' => ['Google', 'Chrome Beta', 'User Data'],
26
+ 'chrome-canary' => ['Google', 'Chrome SxS', 'User Data'],
27
+ 'chrome-dev' => ['Google', 'Chrome Dev', 'User Data'],
28
+ }.freeze #: Hash[String, Array[String]]
29
+
30
+ # @rbs channel: (String | Symbol) -- Chrome release channel
31
+ # @rbs platform: Symbol? -- Optional platform override
32
+ # @rbs env: Hash[String, String] -- Environment variables
33
+ # @rbs home: String? -- Home directory override
34
+ # @rbs return: String -- Default user data directory
35
+ def self.resolve_default(channel, platform: nil, env: ENV, home: nil)
36
+ channel = normalize_channel(channel)
37
+ platform ||= current_platform
38
+ home ||= Dir.home
39
+
40
+ case platform.to_sym
41
+ when :windows
42
+ base = env['LOCALAPPDATA']
43
+ base = join_path(:windows, home, 'AppData', 'Local') if base.nil? || base.empty?
44
+ join_path(:windows, base, *WINDOWS_DIR_NAMES.fetch(channel))
45
+ when :darwin
46
+ join_path(:darwin, home, 'Library', 'Application Support', 'Google', MAC_DIR_NAMES.fetch(channel))
47
+ when :linux
48
+ base = env['CHROME_CONFIG_HOME']
49
+ base = env['XDG_CONFIG_HOME'] if base.nil? || base.empty?
50
+ base = join_path(:linux, home, '.config') if base.nil? || base.empty?
51
+ join_path(:linux, base, LINUX_DIR_NAMES.fetch(channel))
52
+ else
53
+ raise ArgumentError.new("Unsupported platform: #{platform}")
54
+ end
55
+ end
56
+
57
+ # @rbs channel: (String | Symbol) -- Chrome release channel
58
+ # @rbs return: String -- Normalized channel
59
+ def self.normalize_channel(channel)
60
+ normalized_channel = channel.to_s
61
+ return normalized_channel if CHANNELS.include?(normalized_channel)
62
+
63
+ raise ArgumentError.new("Invalid channel: '#{channel}'. Allowed channel is #{CHANNELS}")
64
+ end
65
+
66
+ # @rbs return: Symbol -- Current platform
67
+ def self.current_platform
68
+ if Puppeteer.env.windows?
69
+ :windows
70
+ elsif Puppeteer.env.darwin?
71
+ :darwin
72
+ else
73
+ :linux
74
+ end
75
+ end
76
+ private_class_method :current_platform
77
+
78
+ # @rbs platform: Symbol -- Target platform
79
+ # @rbs parts: String -- Path segments
80
+ # @rbs return: String -- Joined path
81
+ def self.join_path(platform, *parts)
82
+ if platform == :windows
83
+ parts.map(&:to_s).reject(&:empty?).join('\\')
84
+ else
85
+ File.join(*parts)
86
+ end
87
+ end
88
+ private_class_method :join_path
89
+ end
@@ -44,6 +44,7 @@ class Puppeteer::Connection
44
44
  @callbacks_mutex = Mutex.new
45
45
  @delay = delay
46
46
  @protocol_timeout = protocol_timeout
47
+ @reject_emulate_network_conditions_calls = false
47
48
 
48
49
  @network_message_queue = Async::Queue.new
49
50
  @network_message_task = nil
@@ -71,6 +72,16 @@ class Puppeteer::Connection
71
72
  end
72
73
 
73
74
  attr_reader :protocol_timeout
75
+ attr_writer :reject_emulate_network_conditions_calls
76
+
77
+ def ensure_command_allowed!(method)
78
+ return unless method == 'Network.emulateNetworkConditions'
79
+ return unless @reject_emulate_network_conditions_calls
80
+
81
+ raise Puppeteer::Error.new(
82
+ 'Cannot reset network conditions: rule-based emulation is enabled.',
83
+ )
84
+ end
74
85
 
75
86
  # used only in Browser#connected?
76
87
  def closed?
@@ -176,6 +187,7 @@ class Puppeteer::Connection
176
187
  end
177
188
 
178
189
  def async_send_message(method, params = {})
190
+ ensure_command_allowed!(method)
179
191
  promise = Async::Promise.new
180
192
 
181
193
  generate_id do |id|
@@ -332,6 +344,10 @@ class Puppeteer::Connection
332
344
  else
333
345
  callback.resolve(message['result'])
334
346
  end
347
+ else
348
+ sessions = @sessions_mutex.synchronize { @sessions.values }
349
+ session = sessions.find { |candidate| candidate.callback?(message['id']) }
350
+ session&.handle_message(message)
335
351
  end
336
352
  else
337
353
  emit_event(message['method'], message['params'])
@@ -396,3 +412,6 @@ class Puppeteer::Connection
396
412
  @sessions_mutex.synchronize { @sessions[session_id] }.tap { |session| session&.mark_ready }
397
413
  end
398
414
  end
415
+
416
+ # ref: https://github.com/puppeteer/puppeteer/blob/main/packages/puppeteer-core/src/common/Errors.ts
417
+ class Puppeteer::TargetCloseError < Puppeteer::Connection::ProtocolError; end
@@ -5,6 +5,11 @@ class Puppeteer::Coverage
5
5
  @css = Puppeteer::CSSCoverage.new(client)
6
6
  end
7
7
 
8
+ def update_client(client)
9
+ @js.update_client(client)
10
+ @css.update_client(client)
11
+ end
12
+
8
13
  def start_js_coverage(
9
14
  reset_on_navigation: nil,
10
15
  report_anonymous_scripts: nil,
@@ -20,6 +20,10 @@ class Puppeteer::CSSCoverage
20
20
  @stylesheet_sources = {}
21
21
  end
22
22
 
23
+ def update_client(client)
24
+ @client = client
25
+ end
26
+
23
27
  def start(reset_on_navigation: nil)
24
28
  raise 'CSSCoverage is already enabled' if @enabled
25
29
 
@@ -16,6 +16,11 @@ class Puppeteer::Dialog
16
16
  attr_reader :message #: String
17
17
  attr_reader :default_value #: String
18
18
 
19
+ # @rbs return: bool -- Whether the dialog has already been handled
20
+ def handled?
21
+ @handled
22
+ end
23
+
19
24
  # @rbs prompt_text: String? -- Text entered into the prompt
20
25
  # @rbs return: void -- No return value
21
26
  def accept(prompt_text = nil)
@@ -374,14 +374,13 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
374
374
 
375
375
  # @rbs delay: Numeric? -- Delay between down and up (ms)
376
376
  # @rbs button: String? -- Mouse button
377
- # @rbs click_count: Integer? -- Deprecated: use count (click_count only sets clickCount)
378
377
  # @rbs count: Integer? -- Number of clicks to perform
379
378
  # @rbs offset: Puppeteer::ElementHandle::Offset | Hash[Symbol, Numeric] | nil -- Click offset
380
379
  # @rbs return: void -- No return value
381
- def click(delay: nil, button: nil, click_count: nil, count: nil, offset: nil)
380
+ def click(delay: nil, button: nil, count: nil, offset: nil)
382
381
  scroll_into_view_if_needed
383
382
  point = clickable_point(offset)
384
- @page.mouse.click(point.x, point.y, delay: delay, button: button, click_count: click_count, count: count)
383
+ @page.mouse.click(point.x, point.y, delay: delay, button: button, count: count)
385
384
  end
386
385
 
387
386
  define_async_method :async_click