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.
Files changed (61) 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 +1 -0
  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/connection.rb +19 -0
  10. data/lib/puppeteer/coverage.rb +5 -0
  11. data/lib/puppeteer/css_coverage.rb +4 -0
  12. data/lib/puppeteer/dialog.rb +5 -0
  13. data/lib/puppeteer/element_handle.rb +2 -3
  14. data/lib/puppeteer/emulation_manager.rb +70 -9
  15. data/lib/puppeteer/events.rb +2 -0
  16. data/lib/puppeteer/execution_context.rb +7 -0
  17. data/lib/puppeteer/extension.rb +20 -6
  18. data/lib/puppeteer/frame.rb +7 -3
  19. data/lib/puppeteer/frame_manager.rb +71 -26
  20. data/lib/puppeteer/http_response.rb +15 -1
  21. data/lib/puppeteer/isolated_world.rb +10 -4
  22. data/lib/puppeteer/js_coverage.rb +4 -0
  23. data/lib/puppeteer/keyboard.rb +6 -0
  24. data/lib/puppeteer/launcher/browser_options.rb +13 -1
  25. data/lib/puppeteer/launcher/chrome.rb +48 -4
  26. data/lib/puppeteer/lifecycle_watcher.rb +1 -6
  27. data/lib/puppeteer/locators.rb +54 -26
  28. data/lib/puppeteer/mouse.rb +12 -24
  29. data/lib/puppeteer/network_manager.rb +16 -4
  30. data/lib/puppeteer/page.rb +259 -33
  31. data/lib/puppeteer/puppeteer.rb +6 -5
  32. data/lib/puppeteer/screen_recorder.rb +269 -0
  33. data/lib/puppeteer/target.rb +7 -3
  34. data/lib/puppeteer/touch_screen.rb +6 -0
  35. data/lib/puppeteer/tracing.rb +4 -0
  36. data/lib/puppeteer/version.rb +2 -2
  37. data/lib/puppeteer/wait_task.rb +1 -2
  38. data/lib/puppeteer/web_mcp.rb +227 -0
  39. data/lib/puppeteer/web_worker.rb +135 -9
  40. data/lib/puppeteer.rb +11 -1
  41. data/puppeteer-ruby.gemspec +1 -0
  42. data/sig/_supplementary.rbs +33 -1
  43. data/sig/puppeteer/accessibility.rbs +73 -0
  44. data/sig/puppeteer/browser.rbs +27 -3
  45. data/sig/puppeteer/cdp_session.rbs +4 -0
  46. data/sig/puppeteer/dialog.rbs +3 -0
  47. data/sig/puppeteer/element_handle.rbs +1 -2
  48. data/sig/puppeteer/execution_context.rbs +5 -0
  49. data/sig/puppeteer/extension.rbs +4 -0
  50. data/sig/puppeteer/frame.rbs +4 -2
  51. data/sig/puppeteer/http_response.rbs +4 -0
  52. data/sig/puppeteer/keyboard.rbs +4 -0
  53. data/sig/puppeteer/locators.rbs +3 -4
  54. data/sig/puppeteer/mouse.rbs +5 -6
  55. data/sig/puppeteer/page.rbs +48 -4
  56. data/sig/puppeteer/puppeteer.rbs +4 -5
  57. data/sig/puppeteer/screen_recorder.rbs +48 -0
  58. data/sig/puppeteer/touch_screen.rbs +4 -0
  59. data/sig/puppeteer/web_mcp.rbs +112 -0
  60. data/sig/puppeteer/web_worker.rbs +39 -0
  61. metadata +22 -2
@@ -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')
@@ -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
@@ -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
  })