puppeteer-ruby 0.45.6 → 0.50.0.alpha5

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 (98) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -3
  3. data/AGENTS.md +169 -0
  4. data/CLAUDE/README.md +41 -0
  5. data/CLAUDE/architecture.md +253 -0
  6. data/CLAUDE/cdp_protocol.md +230 -0
  7. data/CLAUDE/concurrency.md +216 -0
  8. data/CLAUDE/porting_puppeteer.md +575 -0
  9. data/CLAUDE/rbs_type_checking.md +101 -0
  10. data/CLAUDE/spec_migration_plans.md +1041 -0
  11. data/CLAUDE/testing.md +278 -0
  12. data/CLAUDE.md +242 -0
  13. data/README.md +8 -0
  14. data/Rakefile +7 -0
  15. data/Steepfile +28 -0
  16. data/docs/api_coverage.md +105 -56
  17. data/lib/puppeteer/aria_query_handler.rb +3 -2
  18. data/lib/puppeteer/async_utils.rb +214 -0
  19. data/lib/puppeteer/browser.rb +98 -56
  20. data/lib/puppeteer/browser_connector.rb +18 -3
  21. data/lib/puppeteer/browser_context.rb +196 -3
  22. data/lib/puppeteer/browser_runner.rb +18 -10
  23. data/lib/puppeteer/cdp_session.rb +67 -23
  24. data/lib/puppeteer/chrome_target_manager.rb +65 -40
  25. data/lib/puppeteer/connection.rb +55 -36
  26. data/lib/puppeteer/console_message.rb +9 -1
  27. data/lib/puppeteer/console_patch.rb +47 -0
  28. data/lib/puppeteer/css_coverage.rb +5 -3
  29. data/lib/puppeteer/custom_query_handler.rb +80 -33
  30. data/lib/puppeteer/define_async_method.rb +31 -37
  31. data/lib/puppeteer/dialog.rb +47 -14
  32. data/lib/puppeteer/element_handle.rb +231 -62
  33. data/lib/puppeteer/emulation_manager.rb +1 -1
  34. data/lib/puppeteer/env.rb +1 -1
  35. data/lib/puppeteer/errors.rb +25 -2
  36. data/lib/puppeteer/event_callbackable.rb +15 -0
  37. data/lib/puppeteer/events.rb +4 -0
  38. data/lib/puppeteer/execution_context.rb +148 -3
  39. data/lib/puppeteer/file_chooser.rb +6 -0
  40. data/lib/puppeteer/frame.rb +162 -91
  41. data/lib/puppeteer/frame_manager.rb +69 -48
  42. data/lib/puppeteer/http_request.rb +114 -38
  43. data/lib/puppeteer/http_response.rb +24 -7
  44. data/lib/puppeteer/isolated_world.rb +64 -41
  45. data/lib/puppeteer/js_coverage.rb +5 -3
  46. data/lib/puppeteer/js_handle.rb +58 -16
  47. data/lib/puppeteer/keyboard.rb +30 -17
  48. data/lib/puppeteer/launcher/browser_options.rb +3 -1
  49. data/lib/puppeteer/launcher/chrome.rb +8 -5
  50. data/lib/puppeteer/launcher/launch_options.rb +7 -2
  51. data/lib/puppeteer/launcher.rb +4 -8
  52. data/lib/puppeteer/lifecycle_watcher.rb +38 -22
  53. data/lib/puppeteer/mouse.rb +273 -64
  54. data/lib/puppeteer/network_event_manager.rb +7 -0
  55. data/lib/puppeteer/network_manager.rb +393 -112
  56. data/lib/puppeteer/page/screenshot_task_queue.rb +14 -4
  57. data/lib/puppeteer/page.rb +568 -226
  58. data/lib/puppeteer/puppeteer.rb +171 -64
  59. data/lib/puppeteer/query_handler_manager.rb +112 -16
  60. data/lib/puppeteer/reactor_runner.rb +247 -0
  61. data/lib/puppeteer/remote_object.rb +127 -47
  62. data/lib/puppeteer/target.rb +74 -27
  63. data/lib/puppeteer/task_manager.rb +3 -1
  64. data/lib/puppeteer/timeout_helper.rb +6 -10
  65. data/lib/puppeteer/touch_handle.rb +39 -0
  66. data/lib/puppeteer/touch_screen.rb +72 -22
  67. data/lib/puppeteer/tracing.rb +3 -3
  68. data/lib/puppeteer/version.rb +1 -1
  69. data/lib/puppeteer/wait_task.rb +264 -101
  70. data/lib/puppeteer/web_socket.rb +2 -2
  71. data/lib/puppeteer/web_socket_transport.rb +91 -27
  72. data/lib/puppeteer/web_worker.rb +175 -0
  73. data/lib/puppeteer.rb +20 -4
  74. data/puppeteer-ruby.gemspec +15 -11
  75. data/sig/_external.rbs +8 -0
  76. data/sig/_supplementary.rbs +314 -0
  77. data/sig/puppeteer/browser.rbs +166 -0
  78. data/sig/puppeteer/cdp_session.rbs +64 -0
  79. data/sig/puppeteer/dialog.rbs +41 -0
  80. data/sig/puppeteer/element_handle.rbs +305 -0
  81. data/sig/puppeteer/execution_context.rbs +87 -0
  82. data/sig/puppeteer/frame.rbs +226 -0
  83. data/sig/puppeteer/http_request.rbs +214 -0
  84. data/sig/puppeteer/http_response.rbs +89 -0
  85. data/sig/puppeteer/js_handle.rbs +64 -0
  86. data/sig/puppeteer/keyboard.rbs +40 -0
  87. data/sig/puppeteer/mouse.rbs +113 -0
  88. data/sig/puppeteer/page.rbs +515 -0
  89. data/sig/puppeteer/puppeteer.rbs +98 -0
  90. data/sig/puppeteer/remote_object.rbs +78 -0
  91. data/sig/puppeteer/touch_handle.rbs +21 -0
  92. data/sig/puppeteer/touch_screen.rbs +35 -0
  93. data/sig/puppeteer/web_worker.rbs +83 -0
  94. metadata +116 -45
  95. data/CHANGELOG.md +0 -397
  96. data/lib/puppeteer/concurrent_ruby_utils.rb +0 -81
  97. data/lib/puppeteer/firefox_target_manager.rb +0 -157
  98. data/lib/puppeteer/launcher/firefox.rb +0 -453
@@ -1,5 +1,8 @@
1
+ # rbs_inline: enabled
2
+
1
3
  require 'base64'
2
4
  require 'json'
5
+ require 'objspace'
3
6
  require "stringio"
4
7
 
5
8
  require_relative './page/metrics'
@@ -13,13 +16,14 @@ class Puppeteer::Page
13
16
  include Puppeteer::IfPresent
14
17
  using Puppeteer::DefineAsyncMethod
15
18
 
16
- # @param {!Puppeteer.CDPSession} client
17
- # @param {!Puppeteer.Target} target
18
- # @param {boolean} ignoreHTTPSErrors
19
- # @param {?Puppeteer.Viewport} defaultViewport
20
- # @return {!Promise<!Page>}
21
- def self.create(client, target, ignore_https_errors, default_viewport)
22
- page = Puppeteer::Page.new(client, target, ignore_https_errors)
19
+ # @rbs client: Puppeteer::CDPSession -- CDP session for the page
20
+ # @rbs target: Puppeteer::Target -- Target associated with the page
21
+ # @rbs ignore_https_errors: bool -- Ignore HTTPS errors
22
+ # @rbs default_viewport: Puppeteer::Viewport? -- Default viewport for new pages
23
+ # @rbs network_enabled: bool -- Whether network events are enabled
24
+ # @rbs return: Puppeteer::Page -- Created page instance
25
+ def self.create(client, target, ignore_https_errors, default_viewport, network_enabled: true)
26
+ page = Puppeteer::Page.new(client, target, ignore_https_errors, network_enabled: network_enabled)
23
27
  page.init
24
28
  if default_viewport
25
29
  page.viewport = default_viewport
@@ -27,30 +31,40 @@ class Puppeteer::Page
27
31
  page
28
32
  end
29
33
 
30
- # @param {!Puppeteer.CDPSession} client
31
- # @param {!Puppeteer.Target} target
32
- # @param {boolean} ignoreHTTPSErrors
33
- def initialize(client, target, ignore_https_errors)
34
+ # @rbs client: Puppeteer::CDPSession -- CDP session for the page
35
+ # @rbs target: Puppeteer::Target -- Target associated with the page
36
+ # @rbs ignore_https_errors: bool -- Ignore HTTPS errors
37
+ # @rbs network_enabled: bool -- Whether network events are enabled
38
+ # @rbs return: void -- No return value
39
+ def initialize(client, target, ignore_https_errors, network_enabled: true)
34
40
  @closed = false
35
41
  @client = client
36
42
  @target = target
43
+ @tab_id = nil
37
44
  @keyboard = Puppeteer::Keyboard.new(client)
38
45
  @mouse = Puppeteer::Mouse.new(client, @keyboard)
39
46
  @timeout_settings = Puppeteer::TimeoutSettings.new
40
47
  @touchscreen = Puppeteer::TouchScreen.new(client, @keyboard)
41
48
  # @accessibility = Accessibility.new(client)
42
- @frame_manager = Puppeteer::FrameManager.new(client, self, ignore_https_errors, @timeout_settings)
49
+ @frame_manager = Puppeteer::FrameManager.new(client, self, ignore_https_errors, @timeout_settings, network_enabled: network_enabled)
43
50
  @emulation_manager = Puppeteer::EmulationManager.new(client)
44
51
  @tracing = Puppeteer::Tracing.new(client)
45
52
  @page_bindings = {}
53
+ @page_binding_ids = {}
46
54
  @coverage = Puppeteer::Coverage.new(client)
47
55
  @javascript_enabled = true
48
56
  @screenshot_task_queue = ScreenshotTaskQueue.new
57
+ @inflight_requests = Set.new
58
+ @request_intercepted_listener_map = ObjectSpace::WeakMap.new
59
+ @attached_sessions = Set.new
49
60
 
50
61
  @workers = {}
51
62
  @user_drag_interception_enabled = false
63
+ @service_worker_bypassed = false
52
64
 
53
- @target.target_manager.add_target_interceptor(@client, method(:handle_attached_to_target))
65
+ @attached_session_listener_id = @client.add_event_listener(CDPSessionEmittedEvents::Ready) do |session|
66
+ handle_attached_to_session(session)
67
+ end
54
68
  @target_gone_listener_id = @target.target_manager.add_event_listener(
55
69
  TargetManagerEmittedEvents::TargetGone,
56
70
  &method(:handle_detached_from_target)
@@ -68,15 +82,21 @@ class Puppeteer::Page
68
82
 
69
83
  network_manager = @frame_manager.network_manager
70
84
  network_manager.on_event(NetworkManagerEmittedEvents::Request) do |event|
85
+ @inflight_requests.add(event)
71
86
  emit_event(PageEmittedEvents::Request, event)
72
87
  end
73
88
  network_manager.on_event(NetworkManagerEmittedEvents::Response) do |event|
74
89
  emit_event(PageEmittedEvents::Response, event)
75
90
  end
91
+ network_manager.on_event(NetworkManagerEmittedEvents::RequestServedFromCache) do |event|
92
+ emit_event(PageEmittedEvents::RequestServedFromCache, event)
93
+ end
76
94
  network_manager.on_event(NetworkManagerEmittedEvents::RequestFailed) do |event|
95
+ @inflight_requests.delete(event)
77
96
  emit_event(PageEmittedEvents::RequestFailed, event)
78
97
  end
79
98
  network_manager.on_event(NetworkManagerEmittedEvents::RequestFinished) do |event|
99
+ @inflight_requests.delete(event)
80
100
  emit_event(PageEmittedEvents::RequestFinished, event)
81
101
  end
82
102
  @file_chooser_interception_is_disabled = false
@@ -112,8 +132,9 @@ class Puppeteer::Page
112
132
  @client.on_event('Page.fileChooserOpened') do |event|
113
133
  handle_file_chooser(event)
114
134
  end
115
- @target.is_closed_promise.then do
116
- @target.target_manager.remove_target_interceptor(@client, method(:handle_attached_to_target))
135
+ Async do
136
+ @target.is_closed_promise.wait
137
+ @client.remove_event_listener(@attached_session_listener_id)
117
138
  @target.target_manager.remove_event_listener(@target_gone_listener_id)
118
139
 
119
140
  emit_event(PageEmittedEvents::Close)
@@ -130,63 +151,137 @@ class Puppeteer::Page
130
151
  emit_event(PageEmittedEvents::WorkerDestroyed, worker)
131
152
  end
132
153
 
133
- private def handle_attached_to_target(target, _)
154
+ private def handle_attached_to_session(session)
155
+ return if @attached_sessions.include?(session)
156
+ @attached_sessions << session
157
+ session.on(CDPSessionEmittedEvents::Ready) do |child_session|
158
+ handle_attached_to_session(child_session)
159
+ end
160
+
161
+ target = session.target
162
+ return unless target
163
+ handle_attached_to_target(target)
164
+ end
165
+
166
+ private def handle_attached_to_target(target)
134
167
  @frame_manager.handle_attached_to_target(target)
135
- if target.raw_type == 'worker'
136
- # const session = createdTarget._session();
137
- # assert(session);
138
- # const worker = new WebWorker(
139
- # session,
140
- # createdTarget.url(),
141
- # this.#addConsoleMessage.bind(this),
142
- # this.#handleException.bind(this)
143
- # );
144
- # this.#workers.set(session.id(), worker);
145
- # this.emit(PageEmittedEvents.WorkerCreated, worker);
168
+ session = target.session
169
+ if session && target.raw_type != 'worker'
170
+ @frame_manager.network_manager.add_client(session)
146
171
  end
172
+ if target.raw_type == 'worker'
173
+ return unless session
147
174
 
148
- if target.session
149
- @target.target_manager.add_target_interceptor(target.session, method(:handle_attached_to_target))
175
+ console_api_called = lambda do |world, event|
176
+ values = event['args'].map do |arg|
177
+ remote_object = Puppeteer::RemoteObject.new(arg)
178
+ Puppeteer::JSHandle.create(context: world.execution_context, remote_object: remote_object)
179
+ end
180
+ add_console_message(event['type'], values, event['stackTrace'])
181
+ end
182
+ exception_thrown = method(:handle_exception)
183
+
184
+ worker = Puppeteer::CdpWebWorker.new(
185
+ session,
186
+ target.url,
187
+ target.target_id,
188
+ target.raw_type,
189
+ console_api_called,
190
+ exception_thrown,
191
+ network_manager: @frame_manager.network_manager,
192
+ )
193
+ @workers[session.id] = worker
194
+ emit_event(PageEmittedEvents::WorkerCreated, worker)
150
195
  end
196
+
151
197
  end
152
198
 
199
+ # @rbs return: Array[untyped] -- Initialization results
153
200
  def init
154
- await_all(
201
+ Puppeteer::AsyncUtils.await_promise_all(
155
202
  @frame_manager.async_init(@target.target_id),
156
203
  @client.async_send_message('Performance.enable'),
157
204
  @client.async_send_message('Log.enable'),
158
205
  )
159
206
  end
160
207
 
208
+ # @rbs return: bool -- Whether drag interception is enabled
161
209
  def drag_interception_enabled?
162
210
  @user_drag_interception_enabled
163
211
  end
164
212
  alias_method :drag_interception_enabled, :drag_interception_enabled?
165
213
 
166
- # @param event_name [Symbol]
214
+ # @rbs event_name: (String | Symbol) -- Page event name
215
+ # @rbs &block: ^(untyped) -> void -- Event handler
216
+ # @rbs return: String -- Listener ID
167
217
  def on(event_name, &block)
168
218
  unless PageEmittedEvents.values.include?(event_name.to_s)
169
219
  raise ArgumentError.new("Unknown event name: #{event_name}. Known events are #{PageEmittedEvents.values.to_a.join(", ")}")
170
220
  end
171
221
 
172
222
  if event_name.to_s == 'request'
173
- super('request') do |req|
174
- req.enqueue_intercept_action(-> { block.call(req) })
223
+ wrapped = ->(req) { req.enqueue_intercept_action(-> { block.call(req) }) }
224
+ if (listeners = @request_intercepted_listener_map[block])
225
+ listeners << wrapped
226
+ else
227
+ @request_intercepted_listener_map[block] = [wrapped]
175
228
  end
229
+ super('request', &wrapped)
230
+ else
231
+ super(event_name.to_s, &block)
176
232
  end
177
-
178
- super(event_name.to_s, &block)
179
233
  end
180
234
 
181
- # @param event_name [Symbol]
235
+ # @rbs event_name: (String | Symbol) -- Page event name
236
+ # @rbs &block: ^(untyped) -> void -- Event handler
237
+ # @rbs return: String -- Listener ID
182
238
  def once(event_name, &block)
183
239
  unless PageEmittedEvents.values.include?(event_name.to_s)
184
240
  raise ArgumentError.new("Unknown event name: #{event_name}. Known events are #{PageEmittedEvents.values.to_a.join(", ")}")
185
241
  end
186
242
 
187
- super(event_name.to_s, &block)
243
+ if event_name.to_s == 'request'
244
+ wrapped = ->(req) { req.enqueue_intercept_action(-> { block.call(req) }) }
245
+ if (listeners = @request_intercepted_listener_map[block])
246
+ listeners << wrapped
247
+ else
248
+ @request_intercepted_listener_map[block] = [wrapped]
249
+ end
250
+ super('request', &wrapped)
251
+ else
252
+ super(event_name.to_s, &block)
253
+ end
254
+ end
255
+
256
+ # @rbs event_name_or_id: (String | Symbol) -- Page event name or listener ID
257
+ # @rbs listener: Proc? -- Event handler to remove
258
+ # @rbs return: void -- No return value
259
+ def off(event_name_or_id, listener = nil, &block)
260
+ listener ||= block
261
+ if listener && PageEmittedEvents.values.include?(event_name_or_id.to_s)
262
+ event_name = event_name_or_id.to_s
263
+ if event_name == 'request'
264
+ listeners = @request_intercepted_listener_map[listener]
265
+ wrapped = listeners&.shift
266
+ return unless wrapped
267
+ if listeners.empty?
268
+ if @request_intercepted_listener_map.respond_to?(:delete)
269
+ @request_intercepted_listener_map.delete(listener)
270
+ else
271
+ @request_intercepted_listener_map[listener] = nil
272
+ end
273
+ end
274
+ super(event_name, wrapped)
275
+ else
276
+ super(event_name, listener)
277
+ end
278
+ else
279
+ super(event_name_or_id)
280
+ end
188
281
  end
189
282
 
283
+ # @rbs event: Hash[String, untyped] -- File chooser event payload
284
+ # @rbs return: void -- No return value
190
285
  def handle_file_chooser(event)
191
286
  return if @file_chooser_interceptors.empty?
192
287
 
@@ -196,33 +291,29 @@ class Puppeteer::Page
196
291
  @file_chooser_interceptors.clear
197
292
  file_chooser = Puppeteer::FileChooser.new(element, event)
198
293
  interceptors.each do |promise|
199
- promise.fulfill(file_chooser)
294
+ promise.resolve(file_chooser)
200
295
  end
201
296
  end
202
297
 
203
- class FileChooserTimeoutError < StandardError
204
- def initialize(timeout:)
205
- super("waiting for filechooser failed: timeout #{timeout}ms exceeded")
206
- end
207
- end
208
-
209
- # @param timeout [Integer]
210
- # @return [Puppeteer::FileChooser]
298
+ # @rbs timeout: Numeric? -- Timeout in milliseconds
299
+ # @rbs return: Puppeteer::FileChooser -- File chooser handle
211
300
  def wait_for_file_chooser(timeout: nil)
212
301
  if @file_chooser_interceptors.empty?
213
302
  @client.send_message('Page.setInterceptFileChooserDialog', enabled: true)
214
303
  end
215
304
 
216
305
  option_timeout = timeout || @timeout_settings.timeout
217
- promise = resolvable_future
306
+ promise = Async::Promise.new
218
307
  @file_chooser_interceptors << promise
219
308
 
220
309
  begin
221
- Timeout.timeout(option_timeout / 1000.0) do
222
- promise.value!
310
+ if option_timeout == 0
311
+ promise.wait
312
+ else
313
+ Puppeteer::AsyncUtils.async_timeout(option_timeout, promise).wait
223
314
  end
224
- rescue Timeout::Error
225
- raise FileChooserTimeoutError.new(timeout: option_timeout)
315
+ rescue Async::TimeoutError
316
+ raise Puppeteer::TimeoutError.new("Waiting for `FileChooser` failed: #{option_timeout}ms exceeded")
226
317
  ensure
227
318
  @file_chooser_interceptors.delete(promise)
228
319
  end
@@ -230,23 +321,46 @@ class Puppeteer::Page
230
321
 
231
322
  define_async_method :async_wait_for_file_chooser
232
323
 
233
- # @param [Puppeteer::Geolocation]
324
+ # @rbs geolocation: Puppeteer::Geolocation -- Geolocation override
325
+ # @rbs return: void -- No return value
234
326
  def geolocation=(geolocation)
235
327
  @client.send_message('Emulation.setGeolocationOverride', geolocation.to_h)
236
328
  end
237
329
 
238
- attr_reader :javascript_enabled, :target, :client
330
+ attr_reader :javascript_enabled, :service_worker_bypassed, :target, :client
331
+
332
+ # @rbs return: String -- Tab target id
333
+ def _tab_id
334
+ return @tab_id if @tab_id
335
+
336
+ parent_session = @client.respond_to?(:parent_session) ? @client.parent_session : nil
337
+ @tab_id = parent_session&.target&.target_id || @target.target_id
338
+ end
339
+
340
+ # @rbs other: Object -- Other object to compare
341
+ # @rbs return: bool -- Equality result
342
+ def ==(other)
343
+ other = other.__getobj__ if other.is_a?(Puppeteer::ReactorRunner::Proxy)
344
+ return true if equal?(other)
345
+ return false unless other.is_a?(Puppeteer::Page)
346
+ return false unless @target&.target_id && other.target&.target_id
347
+
348
+ @target.target_id == other.target.target_id
349
+ end
239
350
  alias_method :javascript_enabled?, :javascript_enabled
351
+ alias_method :service_worker_bypassed?, :service_worker_bypassed
240
352
 
353
+ # @rbs return: Puppeteer::Browser -- Owning browser
241
354
  def browser
242
355
  @target.browser
243
356
  end
244
357
 
358
+ # @rbs return: Puppeteer::BrowserContext -- Owning browser context
245
359
  def browser_context
246
360
  @target.browser_context
247
361
  end
248
362
 
249
- class TargetCrashedError < StandardError; end
363
+ class TargetCrashedError < Puppeteer::Error; end
250
364
 
251
365
  private def handle_target_crashed
252
366
  emit_event(PageEmittedEvents::Error, TargetCrashedError.new('Page crashed!'))
@@ -275,58 +389,79 @@ class Puppeteer::Page
275
389
  end
276
390
  end
277
391
 
392
+ # @rbs return: Puppeteer::Frame -- Main frame
278
393
  def main_frame
279
394
  @frame_manager.main_frame
280
395
  end
281
396
 
282
- attr_reader :touch_screen, :coverage, :tracing, :accessibility
397
+ attr_reader :touchscreen, :coverage, :tracing, :accessibility
398
+ alias_method :touch_screen, :touchscreen
283
399
 
400
+ # @rbs block: Proc? -- Optional block for instance_eval
401
+ # @rbs return: Puppeteer::Keyboard -- Keyboard instance
284
402
  def keyboard(&block)
285
403
  @keyboard.instance_eval(&block) unless block.nil?
286
404
 
287
405
  @keyboard
288
406
  end
289
407
 
408
+ # @rbs return: Array[Puppeteer::Frame] -- All frames
290
409
  def frames
291
410
  @frame_manager.frames
292
411
  end
293
412
 
413
+ # @rbs return: Array[untyped] -- Active web workers
294
414
  def workers
295
415
  @workers.values
296
416
  end
297
417
 
298
- # @param value [Bool]
418
+ # @rbs value: bool -- Enable request interception
419
+ # @rbs return: void -- No return value
299
420
  def request_interception=(value)
300
421
  @frame_manager.network_manager.request_interception = value
301
422
  end
302
423
 
424
+ # @rbs enabled: bool -- Enable drag interception
425
+ # @rbs return: void -- No return value
303
426
  def drag_interception_enabled=(enabled)
304
427
  @user_drag_interception_enabled = enabled
305
428
  @client.send_message('Input.setInterceptDrags', enabled: enabled)
306
429
  end
307
430
 
431
+ # @rbs bypass: bool -- Bypass service workers
432
+ # @rbs return: void -- No return value
433
+ def service_worker_bypassed=(bypass)
434
+ @service_worker_bypassed = bypass
435
+ @client.send_message('Network.setBypassServiceWorker', bypass: bypass)
436
+ end
437
+
438
+ # @rbs enabled: bool -- Enable offline mode
439
+ # @rbs return: void -- No return value
308
440
  def offline_mode=(enabled)
309
441
  @frame_manager.network_manager.offline_mode = enabled
310
442
  end
311
443
 
312
- # @param network_condition [Puppeteer::NetworkCondition|nil]
444
+ # @rbs network_condition: Puppeteer::NetworkCondition? -- Network condition override
445
+ # @rbs return: void -- No return value
313
446
  def emulate_network_conditions(network_condition)
314
447
  @frame_manager.network_manager.emulate_network_conditions(network_condition)
315
448
  end
316
449
 
317
- # @param {number} timeout
450
+ # @rbs timeout: Numeric? -- Default navigation timeout in milliseconds
451
+ # @rbs return: void -- No return value
318
452
  def default_navigation_timeout=(timeout)
319
453
  @timeout_settings.default_navigation_timeout = timeout
320
454
  end
321
455
 
322
- # @param {number} timeout
456
+ # @rbs timeout: Numeric? -- Default timeout in milliseconds
457
+ # @rbs return: void -- No return value
323
458
  def default_timeout=(timeout)
324
459
  @timeout_settings.default_timeout = timeout
325
460
  end
326
461
 
327
462
  # `$()` in JavaScript.
328
- # @param {string} selector
329
- # @return {!Promise<?Puppeteer.ElementHandle>}
463
+ # @rbs selector: String -- CSS selector
464
+ # @rbs return: Puppeteer::ElementHandle? -- Matching element or nil
330
465
  def query_selector(selector)
331
466
  main_frame.query_selector(selector)
332
467
  end
@@ -335,18 +470,19 @@ class Puppeteer::Page
335
470
  define_async_method :async_query_selector
336
471
 
337
472
  # `$$()` in JavaScript.
338
- # @param {string} selector
339
- # @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
340
- def query_selector_all(selector)
341
- main_frame.query_selector_all(selector)
473
+ # @rbs selector: String -- CSS selector
474
+ # @rbs isolate: bool? -- Use isolated world for queries
475
+ # @rbs return: Array[Puppeteer::ElementHandle] -- Matching elements
476
+ def query_selector_all(selector, isolate: nil)
477
+ main_frame.query_selector_all(selector, isolate: isolate)
342
478
  end
343
479
  alias_method :SS, :query_selector_all
344
480
 
345
481
  define_async_method :async_query_selector_all
346
482
 
347
- # @param {Function|string} pageFunction
348
- # @param {!Array<*>} args
349
- # @return {!Promise<!Puppeteer.JSHandle>}
483
+ # @rbs page_function: String -- Function or expression to evaluate
484
+ # @rbs args: Array[untyped] -- Arguments for evaluation
485
+ # @rbs return: Puppeteer::JSHandle -- Handle to evaluation result
350
486
  def evaluate_handle(page_function, *args)
351
487
  context = main_frame.execution_context
352
488
  context.evaluate_handle(page_function, *args)
@@ -354,17 +490,18 @@ class Puppeteer::Page
354
490
 
355
491
  define_async_method :async_evaluate_handle
356
492
 
357
- # @param {!Puppeteer.JSHandle} prototypeHandle
358
- # @return {!Promise<!Puppeteer.JSHandle>}
493
+ # @rbs prototype_handle: Puppeteer::JSHandle -- Prototype handle
494
+ # @rbs return: Puppeteer::JSHandle -- Handle to query result
359
495
  def query_objects(prototype_handle)
360
496
  context = main_frame.execution_context
361
497
  context.query_objects(prototype_handle)
362
498
  end
363
499
 
364
500
  # `$eval()` in JavaScript.
365
- # @param selector [String]
366
- # @param page_function [String]
367
- # @return [Object]
501
+ # @rbs selector: String -- CSS selector
502
+ # @rbs page_function: String -- Function or expression to evaluate
503
+ # @rbs args: Array[untyped] -- Arguments for evaluation
504
+ # @rbs return: untyped -- Evaluation result
368
505
  def eval_on_selector(selector, page_function, *args)
369
506
  main_frame.eval_on_selector(selector, page_function, *args)
370
507
  end
@@ -373,9 +510,10 @@ class Puppeteer::Page
373
510
  define_async_method :async_eval_on_selector
374
511
 
375
512
  # `$$eval()` in JavaScript.
376
- # @param selector [String]
377
- # @param page_function [String]
378
- # @return [Object]
513
+ # @rbs selector: String -- CSS selector
514
+ # @rbs page_function: String -- Function or expression to evaluate
515
+ # @rbs args: Array[untyped] -- Arguments for evaluation
516
+ # @rbs return: untyped -- Evaluation result
379
517
  def eval_on_selector_all(selector, page_function, *args)
380
518
  main_frame.eval_on_selector_all(selector, page_function, *args)
381
519
  end
@@ -384,15 +522,16 @@ class Puppeteer::Page
384
522
  define_async_method :async_eval_on_selector_all
385
523
 
386
524
  # `$x()` in JavaScript. $ is not allowed to use as a method name in Ruby.
387
- # @param {string} expression
388
- # @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
525
+ # @rbs expression: String -- XPath expression
526
+ # @rbs return: Array[Puppeteer::ElementHandle] -- Matching elements
389
527
  def Sx(expression)
390
528
  main_frame.Sx(expression)
391
529
  end
392
530
 
393
531
  define_async_method :async_Sx
394
532
 
395
- # @return [Array<Hash>]
533
+ # @rbs urls: Array[String] -- URLs to fetch cookies for
534
+ # @rbs return: Array[Hash[String, untyped]] -- Cookies list
396
535
  def cookies(*urls)
397
536
  @client.send_message('Network.getCookies', urls: (urls.empty? ? [url] : urls))['cookies']
398
537
  end
@@ -406,6 +545,8 @@ class Puppeteer::Page
406
545
  raise ArgumentError.new("Each coookie must have #{requires.join(" and ")} attribute.")
407
546
  end
408
547
 
548
+ # @rbs cookies: Array[Hash[Symbol | String, untyped]] -- cookies parameter
549
+ # @rbs return: void -- No return value
409
550
  def delete_cookie(*cookies)
410
551
  assert_cookie_params(cookies, requires: %i(name))
411
552
 
@@ -417,6 +558,8 @@ class Puppeteer::Page
417
558
  end
418
559
  end
419
560
 
561
+ # @rbs cookies: Array[Hash[Symbol | String, untyped]] -- cookies parameter
562
+ # @rbs return: void -- No return value
420
563
  def set_cookie(*cookies)
421
564
  assert_cookie_params(cookies, requires: %i(name value))
422
565
 
@@ -434,24 +577,27 @@ class Puppeteer::Page
434
577
  end
435
578
  end
436
579
 
437
- # @param url [String?]
438
- # @param path [String?]
439
- # @param content [String?]
440
- # @param type [String?]
441
- # @param id [String?]
580
+ # @rbs url: String? -- Script URL
581
+ # @rbs path: String? -- Path to script file
582
+ # @rbs content: String? -- Script contents
583
+ # @rbs type: String? -- Script type
584
+ # @rbs id: String? -- Script element ID
585
+ # @rbs return: Puppeteer::ElementHandle -- Script element handle
442
586
  def add_script_tag(url: nil, path: nil, content: nil, type: nil, id: nil)
443
587
  main_frame.add_script_tag(url: url, path: path, content: content, type: type, id: id)
444
588
  end
445
589
 
446
- # @param url [String?]
447
- # @param path [String?]
448
- # @param content [String?]
590
+ # @rbs url: String? -- Stylesheet URL
591
+ # @rbs path: String? -- Path to stylesheet file
592
+ # @rbs content: String? -- Stylesheet contents
593
+ # @rbs return: Puppeteer::ElementHandle -- Style element handle
449
594
  def add_style_tag(url: nil, path: nil, content: nil)
450
595
  main_frame.add_style_tag(url: url, path: path, content: content)
451
596
  end
452
597
 
453
- # @param name [String]
454
- # @param puppeteer_function [Proc]
598
+ # @rbs name: String -- Binding name
599
+ # @rbs puppeteer_function: Proc -- Ruby callback
600
+ # @rbs return: void -- No return value
455
601
  def expose_function(name, puppeteer_function)
456
602
  if @page_bindings[name]
457
603
  raise ArgumentError.new("Failed to add page binding with name `#{name}` already exists!")
@@ -486,42 +632,79 @@ class Puppeteer::Page
486
632
 
487
633
  source = JavaScriptFunction.new(add_page_binding, ['exposedFun', name]).source
488
634
  @client.send_message('Runtime.addBinding', name: name)
489
- @client.send_message('Page.addScriptToEvaluateOnNewDocument', source: source)
635
+ script = @client.send_message('Page.addScriptToEvaluateOnNewDocument', source: source)
636
+ @page_binding_ids[name] = script['identifier']
490
637
 
491
638
  promises = @frame_manager.frames.map do |frame|
492
639
  frame.async_evaluate("() => #{source}")
493
640
  end
494
- await_all(*promises)
641
+ Puppeteer::AsyncUtils.await_promise_all(*promises)
642
+
643
+ nil
644
+ end
645
+
646
+ # @rbs name: String -- Binding name
647
+ # @rbs return: void -- No return value
648
+ def remove_exposed_function(name)
649
+ identifier = @page_binding_ids[name]
650
+ unless identifier
651
+ raise ArgumentError.new("Function with name \"#{name}\" does not exist")
652
+ end
653
+
654
+ @page_binding_ids.delete(name)
655
+ @page_bindings.delete(name)
656
+
657
+ @client.send_message('Runtime.removeBinding', name: name)
658
+ @client.send_message('Page.removeScriptToEvaluateOnNewDocument', identifier: identifier)
495
659
 
660
+ remove_script = '(name) => { delete window[name]; }'
661
+ @frame_manager.frames.each do |frame|
662
+ frame.evaluate(remove_script, name)
663
+ rescue StandardError
664
+ nil
665
+ end
496
666
  nil
497
667
  end
498
668
 
499
- # @param username [String?]
500
- # @param password [String?]
669
+ # @rbs username: String? -- HTTP basic auth username
670
+ # @rbs password: String? -- HTTP basic auth password
671
+ # @rbs return: void -- No return value
501
672
  def authenticate(username: nil, password: nil)
502
673
  @frame_manager.network_manager.authenticate(username: username, password: password)
503
674
  end
504
675
 
505
- # @param headers [Hash]
676
+ # @rbs headers: Hash[String, String] -- Extra HTTP headers
677
+ # @rbs return: void -- No return value
506
678
  def extra_http_headers=(headers)
507
679
  @frame_manager.network_manager.extra_http_headers = headers
508
680
  end
509
681
 
510
- # @param user_agent [String]
511
- # @param user_agent_metadata [Hash]
682
+ # @rbs user_agent: String -- User agent string
683
+ # @rbs user_agent_metadata: Hash[String, untyped]? -- User agent metadata
684
+ # @rbs return: void -- No return value
512
685
  def set_user_agent(user_agent, user_agent_metadata = nil)
513
686
  @frame_manager.network_manager.set_user_agent(user_agent, user_agent_metadata)
514
687
  end
515
688
  alias_method :user_agent=, :set_user_agent
516
689
 
690
+ # @rbs return: Puppeteer::Page::Metrics -- Page metrics
517
691
  def metrics
518
692
  response = @client.send_message('Performance.getMetrics')
519
693
  Metrics.new(response['metrics'])
520
694
  end
521
695
 
522
- class PageError < StandardError ; end
696
+ class PageError < Puppeteer::Error ; end
523
697
 
524
698
  private def handle_exception(exception_details)
699
+ exception = exception_details['exception']
700
+ if exception
701
+ is_error_object = exception['type'] == 'object' && exception['subtype'] == 'error'
702
+ if !is_error_object && !exception.key?('objectId')
703
+ emit_event(PageEmittedEvents::PageError, Puppeteer::RemoteObject.new(exception).value)
704
+ return
705
+ end
706
+ end
707
+
525
708
  message = Puppeteer::ExceptionDetails.new(exception_details).message
526
709
  err = PageError.new(message)
527
710
  # err.stack = ''; // Don't report clientside error with a node stack attached
@@ -554,6 +737,8 @@ class Puppeteer::Page
554
737
  add_console_message(event['type'], values, event['stackTrace'])
555
738
  end
556
739
 
740
+ # @rbs event: Hash[String, untyped] -- Binding called payload
741
+ # @rbs return: void -- No return value
557
742
  def handle_binding_called(event)
558
743
  execution_context_id = event['executionContextId']
559
744
  payload =
@@ -595,13 +780,18 @@ class Puppeteer::Page
595
780
  JavaScriptFunction.new(deliver_error, [name, seq, err.message]).source
596
781
  end
597
782
 
598
- @client.async_send_message('Runtime.evaluate', expression: expression, contextId: execution_context_id).rescue do |error|
783
+ Async do
784
+ @client.async_send_message('Runtime.evaluate', expression: expression, contextId: execution_context_id).wait
785
+ rescue => error
599
786
  debug_puts(error)
600
787
  end
601
788
  end
602
789
 
603
790
  private def add_console_message(type, args, stack_trace)
604
- text_tokens = args.map { |arg| arg.remote_object.value }
791
+ text_tokens = args.map do |arg|
792
+ value = arg.remote_object.value
793
+ value.nil? ? arg.to_s : value
794
+ end
605
795
 
606
796
  stack_trace_locations =
607
797
  if stack_trace && stack_trace['callFrames']
@@ -624,7 +814,7 @@ class Puppeteer::Page
624
814
  unless %w(alert confirm prompt beforeunload).include?(dialog_type)
625
815
  raise ArgumentError.new("Unknown javascript dialog type: #{dialog_type}")
626
816
  end
627
- dialog = Puppeteer::Dialog.new(@client,
817
+ dialog = Puppeteer::CdpDialog.new(@client,
628
818
  type: dialog_type,
629
819
  message: event['message'],
630
820
  default_value: event['defaultPrompt'])
@@ -641,72 +831,101 @@ class Puppeteer::Page
641
831
  @client.send_message('Emulation.setDefaultBackgroundColorOverride')
642
832
  end
643
833
 
644
- # @return [String]
834
+ # @rbs return: String? -- Page URL
645
835
  def url
646
836
  main_frame.url
647
837
  end
648
838
 
649
- # @return [String]
839
+ # @rbs return: String -- Page HTML content
650
840
  def content
651
841
  main_frame.content
652
842
  end
653
843
 
654
- # @param html [String]
655
- # @param timeout [Integer]
656
- # @param wait_until [String|Array<String>]
844
+ # @rbs html: String -- HTML content
845
+ # @rbs timeout: Numeric? -- Navigation timeout in milliseconds
846
+ # @rbs wait_until: String | Array[String] | nil -- Lifecycle events to wait for
847
+ # @rbs return: void -- No return value
657
848
  def set_content(html, timeout: nil, wait_until: nil)
658
849
  main_frame.set_content(html, timeout: timeout, wait_until: wait_until)
659
850
  end
660
851
 
661
- # @param html [String]
852
+ # @rbs html: String -- HTML content
853
+ # @rbs return: void -- No return value
662
854
  def content=(html)
663
855
  main_frame.set_content(html)
664
856
  end
665
857
 
666
- # @param url [String]
667
- # @param rederer [String]
668
- # @param timeout [number|nil]
669
- # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
670
- def goto(url, referer: nil, timeout: nil, wait_until: nil)
671
- main_frame.goto(url, referer: referer, timeout: timeout, wait_until: wait_until)
858
+ # @rbs url: String -- URL to navigate
859
+ # @rbs referer: String? -- Referer header value
860
+ # @rbs referer: String? -- Referer header value
861
+ # @rbs referrer_policy: String? -- Referrer policy
862
+ # @rbs timeout: Numeric? -- Navigation timeout in milliseconds
863
+ # @rbs wait_until: String | Array[String] | nil -- Lifecycle events to wait for
864
+ # @rbs return: Puppeteer::HTTPResponse? -- Navigation response
865
+ def goto(url, referer: nil, referrer_policy: nil, timeout: nil, wait_until: nil)
866
+ main_frame.goto(
867
+ url,
868
+ referer: referer,
869
+ referrer_policy: referrer_policy,
870
+ timeout: timeout,
871
+ wait_until: wait_until,
872
+ )
672
873
  end
673
874
 
674
- # @param timeout [number|nil]
675
- # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
676
- # @return [Puppeteer::HTTPResponse]
677
- def reload(timeout: nil, wait_until: nil)
678
- wait_for_navigation(timeout: timeout, wait_until: wait_until) do
679
- @client.send_message('Page.reload')
875
+ # @rbs timeout: Numeric? -- Navigation timeout in milliseconds
876
+ # @rbs wait_until: String | Array[String] | nil -- Lifecycle events to wait for
877
+ # @rbs ignore_cache: bool? -- Skip cache when reloading
878
+ # @rbs return: Puppeteer::HTTPResponse? -- Navigation response
879
+ def reload(timeout: nil, wait_until: nil, ignore_cache: nil)
880
+ params = {}
881
+ params[:ignoreCache] = ignore_cache unless ignore_cache.nil?
882
+
883
+ wait_for_navigation(timeout: timeout, wait_until: wait_until, ignore_same_document_navigation: true) do
884
+ if params.empty?
885
+ @client.send_message('Page.reload')
886
+ else
887
+ @client.send_message('Page.reload', **params)
888
+ end
680
889
  end
681
890
  end
682
891
 
683
- def wait_for_navigation(timeout: nil, wait_until: nil)
684
- main_frame.send(:wait_for_navigation, timeout: timeout, wait_until: wait_until)
892
+ # @rbs timeout: Numeric? -- Navigation timeout in milliseconds
893
+ # @rbs wait_until: String | Array[String] | nil -- Lifecycle events to wait for
894
+ # @rbs ignore_same_document_navigation: bool -- Ignore same-document navigation
895
+ # @rbs return: Puppeteer::HTTPResponse? -- Navigation response
896
+ def wait_for_navigation(timeout: nil, wait_until: nil, ignore_same_document_navigation: false)
897
+ main_frame.send(
898
+ :wait_for_navigation,
899
+ timeout: timeout,
900
+ wait_until: wait_until,
901
+ ignore_same_document_navigation: ignore_same_document_navigation,
902
+ )
685
903
  end
686
904
 
687
905
  # @!method async_wait_for_navigation(timeout: nil, wait_until: nil)
688
906
  #
689
- # @param timeout [number|nil]
690
- # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
691
907
  define_async_method :async_wait_for_navigation
692
908
 
693
909
  private def wait_for_network_manager_event(event_name, predicate:, timeout:)
694
910
  option_timeout = timeout || @timeout_settings.timeout
695
911
 
696
- promise = resolvable_future
912
+ promise = Async::Promise.new
697
913
 
698
914
  listener_id = @frame_manager.network_manager.add_event_listener(event_name) do |event_target|
699
- if predicate.call(event_target)
700
- promise.fulfill(event_target)
915
+ if Puppeteer::AsyncUtils.await(predicate.call(event_target))
916
+ promise.resolve(event_target)
701
917
  end
702
918
  end
703
919
 
704
920
  begin
705
- # Timeout.timeout(0) means "no limit" for timeout.
706
- Timeout.timeout(option_timeout / 1000.0) do
707
- await_any(promise, session_close_promise)
921
+ if option_timeout == 0
922
+ Puppeteer::AsyncUtils.await_promise_race(promise, session_close_promise)
923
+ else
924
+ Puppeteer::AsyncUtils.async_timeout(option_timeout, -> {
925
+ Puppeteer::AsyncUtils.await_promise_race(promise, session_close_promise)
926
+ }).wait
708
927
  end
709
- rescue Timeout::Error
928
+ rescue Async::TimeoutError
710
929
  raise Puppeteer::TimeoutError.new("waiting for #{event_name} failed: timeout #{option_timeout}ms exceeded")
711
930
  ensure
712
931
  @frame_manager.network_manager.remove_event_listener(listener_id)
@@ -716,22 +935,25 @@ class Puppeteer::Page
716
935
  private def wait_for_frame_manager_event(*event_names, predicate:, timeout:)
717
936
  option_timeout = timeout || @timeout_settings.timeout
718
937
 
719
- promise = resolvable_future
938
+ promise = Async::Promise.new
720
939
 
721
940
  listener_ids = event_names.map do |event_name|
722
941
  @frame_manager.add_event_listener(event_name) do |event_target|
723
- if predicate.call(event_target)
724
- promise.fulfill(event_target) unless promise.resolved?
942
+ if Puppeteer::AsyncUtils.await(predicate.call(event_target))
943
+ promise.resolve(event_target) unless promise.resolved?
725
944
  end
726
945
  end
727
946
  end
728
947
 
729
948
  begin
730
- # Timeout.timeout(0) means "no limit" for timeout.
731
- Timeout.timeout(option_timeout / 1000.0) do
732
- await_any(promise, session_close_promise)
949
+ if option_timeout == 0
950
+ Puppeteer::AsyncUtils.await_promise_race(promise, session_close_promise)
951
+ else
952
+ Puppeteer::AsyncUtils.async_timeout(option_timeout, -> {
953
+ Puppeteer::AsyncUtils.await_promise_race(promise, session_close_promise)
954
+ }).wait
733
955
  end
734
- rescue Timeout::Error
956
+ rescue Async::TimeoutError
735
957
  raise Puppeteer::TimeoutError.new("waiting for #{event_names.join(" or ")} failed: timeout #{option_timeout}ms exceeded")
736
958
  ensure
737
959
  listener_ids.each do |listener_id|
@@ -741,13 +963,17 @@ class Puppeteer::Page
741
963
  end
742
964
 
743
965
  private def session_close_promise
744
- @disconnect_promise ||= resolvable_future do |future|
966
+ @disconnect_promise ||= Async::Promise.new.tap do |future|
745
967
  @client.observe_first(CDPSessionEmittedEvents::Disconnected) do
746
968
  future.reject(Puppeteer::CDPSession::Error.new('Target Closed'))
747
969
  end
748
970
  end
749
971
  end
750
972
 
973
+ # @rbs url: String? -- URL to match
974
+ # @rbs predicate: Proc? -- Predicate to match
975
+ # @rbs timeout: Numeric? -- Timeout in milliseconds
976
+ # @rbs return: Puppeteer::HTTPRequest -- Matching request
751
977
  def wait_for_request(url: nil, predicate: nil, timeout: nil)
752
978
  if !url && !predicate
753
979
  raise ArgumentError.new('url or predicate must be specified')
@@ -778,10 +1004,12 @@ class Puppeteer::Page
778
1004
  # Waits until request matches the given predicate
779
1005
  # wait_for_request(predicate: -> (req){ req.url.start_with?('https://example.com/search') })
780
1006
  #
781
- # @param url [String]
782
- # @param predicate [Proc(Puppeteer::HTTPRequest -> Boolean)]
783
1007
  define_async_method :async_wait_for_request
784
1008
 
1009
+ # @rbs url: String? -- URL to match
1010
+ # @rbs predicate: Proc? -- Predicate to match
1011
+ # @rbs timeout: Numeric? -- Timeout in milliseconds
1012
+ # @rbs return: Puppeteer::HTTPResponse -- Matching response
785
1013
  def wait_for_response(url: nil, predicate: nil, timeout: nil)
786
1014
  if !url && !predicate
787
1015
  raise ArgumentError.new('url or predicate must be specified')
@@ -804,10 +1032,68 @@ class Puppeteer::Page
804
1032
 
805
1033
  # @!method async_wait_for_response(url: nil, predicate: nil, timeout: nil)
806
1034
  #
807
- # @param url [String]
808
- # @param predicate [Proc(Puppeteer::HTTPRequest -> Boolean)]
809
1035
  define_async_method :async_wait_for_response
810
1036
 
1037
+ # @rbs idle_time: Numeric -- Idle time to wait for in milliseconds
1038
+ # @rbs timeout: Numeric? -- Timeout in milliseconds
1039
+ # @rbs concurrency: Integer -- Allowed number of concurrent requests
1040
+ # @rbs return: void -- No return value
1041
+ def wait_for_network_idle(idle_time: 500, timeout: nil, concurrency: 0)
1042
+ option_timeout = timeout || @timeout_settings.timeout
1043
+
1044
+ promise = Async::Promise.new
1045
+ idle_timer = nil
1046
+
1047
+ schedule_idle = lambda do
1048
+ return if @inflight_requests.size > concurrency
1049
+
1050
+ idle_timer&.stop
1051
+ idle_timer = Async do
1052
+ Puppeteer::AsyncUtils.sleep_seconds(idle_time / 1000.0)
1053
+ unless promise.resolved? || @inflight_requests.size > concurrency
1054
+ promise.resolve(nil)
1055
+ end
1056
+ end
1057
+ end
1058
+
1059
+ # Use raw listener to avoid request interception queue delaying idle tracking.
1060
+ request_listener = add_event_listener('request') do
1061
+ idle_timer&.stop
1062
+ idle_timer = nil
1063
+ end
1064
+ request_finished_listener = on('requestfinished') do
1065
+ schedule_idle.call
1066
+ end
1067
+ request_failed_listener = on('requestfailed') do
1068
+ schedule_idle.call
1069
+ end
1070
+
1071
+ schedule_idle.call
1072
+
1073
+ begin
1074
+ if option_timeout == 0
1075
+ Puppeteer::AsyncUtils.await_promise_race(promise, session_close_promise)
1076
+ else
1077
+ Puppeteer::AsyncUtils.async_timeout(option_timeout, -> {
1078
+ Puppeteer::AsyncUtils.await_promise_race(promise, session_close_promise)
1079
+ }).wait
1080
+ end
1081
+ rescue Async::TimeoutError
1082
+ raise Puppeteer::TimeoutError.new("waiting for network idle failed: timeout #{option_timeout}ms exceeded")
1083
+ ensure
1084
+ off(request_listener)
1085
+ off(request_finished_listener)
1086
+ off(request_failed_listener)
1087
+ idle_timer&.stop
1088
+ end
1089
+ end
1090
+
1091
+ define_async_method :async_wait_for_network_idle
1092
+
1093
+ # @rbs url: String? -- URL to match
1094
+ # @rbs predicate: Proc? -- Predicate to match
1095
+ # @rbs timeout: Numeric? -- Timeout in milliseconds
1096
+ # @rbs return: Puppeteer::Frame -- Matching frame
811
1097
  def wait_for_frame(url: nil, predicate: nil, timeout: nil)
812
1098
  if !url && !predicate
813
1099
  raise ArgumentError.new('url or predicate must be specified')
@@ -836,18 +1122,18 @@ class Puppeteer::Page
836
1122
 
837
1123
  # @!method async_wait_for_frame(url: nil, predicate: nil, timeout: nil)
838
1124
  #
839
- # @param url [String]
840
- # @param predicate [Proc(Puppeteer::Frame -> Boolean)]
841
1125
  define_async_method :async_wait_for_frame
842
1126
 
843
- # @param timeout [number|nil]
844
- # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
1127
+ # @rbs timeout: Numeric? -- Navigation timeout in milliseconds
1128
+ # @rbs wait_until: String | Array[String] | nil -- Lifecycle events to wait for
1129
+ # @rbs return: Puppeteer::HTTPResponse? -- Navigation response
845
1130
  def go_back(timeout: nil, wait_until: nil)
846
1131
  go(-1, timeout: timeout, wait_until: wait_until)
847
1132
  end
848
1133
 
849
- # @param timeout [number|nil]
850
- # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
1134
+ # @rbs timeout: Numeric? -- Navigation timeout in milliseconds
1135
+ # @rbs wait_until: String | Array[String] | nil -- Lifecycle events to wait for
1136
+ # @rbs return: Puppeteer::HTTPResponse? -- Navigation response
851
1137
  def go_forward(timeout: nil, wait_until: nil)
852
1138
  go(+1, timeout: timeout, wait_until: wait_until)
853
1139
  end
@@ -856,37 +1142,44 @@ class Puppeteer::Page
856
1142
  history = @client.send_message('Page.getNavigationHistory')
857
1143
  entries = history['entries']
858
1144
  index = history['currentIndex'] + delta
859
- if_present(entries[index]) do |entry|
860
- wait_for_navigation(timeout: timeout, wait_until: wait_until) do
861
- @client.send_message('Page.navigateToHistoryEntry', entryId: entry['id'])
862
- end
1145
+ if index < 0 || index >= entries.length
1146
+ raise Puppeteer::Error.new('History entry to navigate to not found.')
1147
+ end
1148
+ entry = entries[index]
1149
+ wait_for_navigation(timeout: timeout, wait_until: wait_until) do
1150
+ @client.send_message('Page.navigateToHistoryEntry', entryId: entry['id'])
863
1151
  end
864
1152
  end
865
1153
 
866
1154
  # Brings page to front (activates tab).
1155
+ # @rbs return: void -- No return value
867
1156
  def bring_to_front
868
1157
  @client.send_message('Page.bringToFront')
869
1158
  end
870
1159
 
871
- # @param device [Device]
1160
+ # @rbs device: Puppeteer::Device -- Device descriptor
1161
+ # @rbs return: void -- No return value
872
1162
  def emulate(device)
873
1163
  self.viewport = device.viewport
874
1164
  self.user_agent = device.user_agent
875
1165
  end
876
1166
 
877
- # @param {boolean} enabled
1167
+ # @rbs enabled: bool -- Enable JavaScript
1168
+ # @rbs return: void -- No return value
878
1169
  def javascript_enabled=(enabled)
879
1170
  return if @javascript_enabled == enabled
880
1171
  @javascript_enabled = enabled
881
1172
  @client.send_message('Emulation.setScriptExecutionDisabled', value: !enabled)
882
1173
  end
883
1174
 
884
- # @param enabled [Boolean]
1175
+ # @rbs enabled: bool -- Enable bypassing CSP
1176
+ # @rbs return: void -- No return value
885
1177
  def bypass_csp=(enabled)
886
1178
  @client.send_message('Page.setBypassCSP', enabled: enabled)
887
1179
  end
888
1180
 
889
- # @param media_type [String|Symbol|nil] either of (media, print, nil)
1181
+ # @rbs media_type: (String | Symbol)? -- Media type override
1182
+ # @rbs return: void -- No return value
890
1183
  def emulate_media_type(media_type)
891
1184
  media_type_str = media_type.to_s
892
1185
  unless ['screen', 'print', ''].include?(media_type_str)
@@ -895,7 +1188,8 @@ class Puppeteer::Page
895
1188
  @client.send_message('Emulation.setEmulatedMedia', media: media_type_str)
896
1189
  end
897
1190
 
898
- # @param factor [Number|nil] Factor at which the CPU will be throttled (2x, 2.5x. 3x, ...). Passing `nil` disables cpu throttling.
1191
+ # @rbs factor: Numeric? -- CPU throttling rate
1192
+ # @rbs return: void -- No return value
899
1193
  def emulate_cpu_throttling(factor)
900
1194
  if factor.nil? || factor >= 1
901
1195
  @client.send_message('Emulation.setCPUThrottlingRate', rate: factor || 1)
@@ -904,7 +1198,8 @@ class Puppeteer::Page
904
1198
  end
905
1199
  end
906
1200
 
907
- # @param features [Array]
1201
+ # @rbs features: Array[Hash[Symbol, untyped]]? -- Media feature overrides
1202
+ # @rbs return: void -- No return value
908
1203
  def emulate_media_features(features)
909
1204
  if features.nil?
910
1205
  @client.send_message('Emulation.setEmulatedMedia', features: nil)
@@ -919,7 +1214,8 @@ class Puppeteer::Page
919
1214
  end
920
1215
  end
921
1216
 
922
- # @param timezone_id [String?]
1217
+ # @rbs timezone_id: String? -- Timezone ID
1218
+ # @rbs return: void -- No return value
923
1219
  def emulate_timezone(timezone_id)
924
1220
  @client.send_message('Emulation.setTimezoneOverride', timezoneId: timezone_id || '')
925
1221
  rescue => err
@@ -939,6 +1235,8 @@ class Puppeteer::Page
939
1235
  tritanopia
940
1236
  ].freeze
941
1237
 
1238
+ # @rbs vision_deficiency_type: String? -- Vision deficiency type
1239
+ # @rbs return: void -- No return value
942
1240
  def emulate_vision_deficiency(vision_deficiency_type)
943
1241
  value = vision_deficiency_type || 'none'
944
1242
  unless VISION_DEFICIENCY_TYPES.include?(value)
@@ -947,8 +1245,9 @@ class Puppeteer::Page
947
1245
  @client.send_message('Emulation.setEmulatedVisionDeficiency', type: value)
948
1246
  end
949
1247
 
950
- # @param is_user_active [Boolean]
951
- # @param is_screen_unlocked [Boolean]
1248
+ # @rbs is_user_active: bool? -- User activity override
1249
+ # @rbs is_screen_unlocked: bool? -- Screen unlocked override
1250
+ # @rbs return: void -- No return value
952
1251
  def emulate_idle_state(is_user_active: nil, is_screen_unlocked: nil)
953
1252
  overrides = {
954
1253
  isUserActive: is_user_active,
@@ -962,7 +1261,8 @@ class Puppeteer::Page
962
1261
  end
963
1262
  end
964
1263
 
965
- # @param viewport [Viewport]
1264
+ # @rbs viewport: Puppeteer::Viewport? -- Viewport settings
1265
+ # @rbs return: void -- No return value
966
1266
  def viewport=(viewport)
967
1267
  needs_reload = @emulation_manager.emulate_viewport(viewport)
968
1268
  @viewport = viewport
@@ -971,9 +1271,9 @@ class Puppeteer::Page
971
1271
 
972
1272
  attr_reader :viewport
973
1273
 
974
- # @param {Function|string} pageFunction
975
- # @param {!Array<*>} args
976
- # @return {!Promise<*>}
1274
+ # @rbs page_function: String -- page_function parameter
1275
+ # @rbs args: Array[untyped] -- args parameter
1276
+ # @rbs return: untyped -- Result
977
1277
  def evaluate(page_function, *args)
978
1278
  main_frame.evaluate(page_function, *args)
979
1279
  end
@@ -981,11 +1281,15 @@ class Puppeteer::Page
981
1281
  define_async_method :async_evaluate
982
1282
 
983
1283
  class JavaScriptFunction
1284
+ # @rbs expression: String -- Function expression
1285
+ # @rbs args: Array[untyped] -- Arguments for evaluation
1286
+ # @rbs return: void -- No return value
984
1287
  def initialize(expression, args)
985
1288
  @expression = expression
986
1289
  @args = args
987
1290
  end
988
1291
 
1292
+ # @rbs return: String -- Generated source
989
1293
  def source
990
1294
  "(#{@expression})(#{arguments})"
991
1295
  end
@@ -996,15 +1300,21 @@ class Puppeteer::Page
996
1300
  end
997
1301
 
998
1302
  class JavaScriptExpression
1303
+ # @rbs expression: String -- Expression to evaluate
1304
+ # @rbs return: void -- No return value
999
1305
  def initialize(expression)
1000
1306
  @expression = expression
1001
1307
  end
1002
1308
 
1309
+ # @rbs return: String -- Generated source
1003
1310
  def source
1004
1311
  @expression
1005
1312
  end
1006
1313
  end
1007
1314
 
1315
+ # @rbs page_function: String -- page_function parameter
1316
+ # @rbs args: Array[untyped] -- args parameter
1317
+ # @rbs return: Hash[String, untyped] -- CDP response
1008
1318
  def evaluate_on_new_document(page_function, *args)
1009
1319
  source =
1010
1320
  if ['=>', 'async', 'function'].any? { |keyword| page_function.include?(keyword) }
@@ -1016,23 +1326,32 @@ class Puppeteer::Page
1016
1326
  @client.send_message('Page.addScriptToEvaluateOnNewDocument', source: source)
1017
1327
  end
1018
1328
 
1019
- # @param {boolean} enabled
1329
+ # @rbs identifier: String -- Script identifier to remove
1330
+ # @rbs return: void
1331
+ def remove_script_to_evaluate_on_new_document(identifier)
1332
+ @client.send_message('Page.removeScriptToEvaluateOnNewDocument', identifier: identifier)
1333
+ end
1334
+
1335
+ # @rbs enabled: bool -- Enable cache usage
1020
1336
  def cache_enabled=(enabled)
1021
1337
  @frame_manager.network_manager.cache_enabled = enabled
1022
1338
  end
1023
1339
 
1024
- # @return [String]
1340
+ # @rbs return: String -- Page title
1025
1341
  def title
1026
1342
  main_frame.title
1027
1343
  end
1028
1344
 
1029
- # @param type [String] "png"|"jpeg"|"webp"
1030
- # @param path [String]
1031
- # @param full_page [Boolean]
1032
- # @param clip [Hash]
1033
- # @param quality [Integer]
1034
- # @param omit_background [Boolean]
1035
- # @param encoding [String]
1345
+ # @rbs type: String? -- Image format
1346
+ # @rbs path: String? -- File path to save
1347
+ # @rbs full_page: bool? -- Capture full page
1348
+ # @rbs clip: Hash[Symbol, Numeric]? -- Clip rectangle
1349
+ # @rbs quality: Integer? -- JPEG quality
1350
+ # @rbs omit_background: bool? -- Omit background for PNG
1351
+ # @rbs encoding: String? -- Encoding (base64 or binary)
1352
+ # @rbs capture_beyond_viewport: bool? -- Capture beyond viewport
1353
+ # @rbs from_surface: bool? -- Capture from surface
1354
+ # @rbs return: String -- Screenshot data
1036
1355
  def screenshot(type: nil,
1037
1356
  path: nil,
1038
1357
  full_page: nil,
@@ -1055,14 +1374,14 @@ class Puppeteer::Page
1055
1374
  }.compact
1056
1375
  screenshot_options = ScreenshotOptions.new(options)
1057
1376
 
1377
+ guard = browser_context.start_screenshot
1058
1378
  @screenshot_task_queue.post_task do
1059
1379
  screenshot_task(screenshot_options.type, screenshot_options)
1060
1380
  end
1381
+ ensure
1382
+ guard&.release
1061
1383
  end
1062
1384
 
1063
- # @param {"png"|"jpeg"} format
1064
- # @param {!ScreenshotOptions=} options
1065
- # @return {!Promise<!Buffer|!String>}
1066
1385
  private def screenshot_task(format, screenshot_options)
1067
1386
  @client.send_message('Target.activateTarget', targetId: @target.target_id)
1068
1387
 
@@ -1129,13 +1448,15 @@ class Puppeteer::Page
1129
1448
  buffer
1130
1449
  end
1131
1450
 
1132
- class PrintToPdfIsNotImplementedError < StandardError
1451
+ class PrintToPdfIsNotImplementedError < Puppeteer::Error
1452
+ # @rbs return: void -- No return value
1133
1453
  def initialize
1134
1454
  super('pdf() is only available in headless mode. See https://github.com/puppeteer/puppeteer/issues/1829')
1135
1455
  end
1136
1456
  end
1137
1457
 
1138
- # @return [Enumerable<String>]
1458
+ # @rbs options: Hash[Symbol, untyped] -- PDF options
1459
+ # @rbs return: Enumerable[String] -- PDF data chunks
1139
1460
  def create_pdf_stream(options = {})
1140
1461
  timeout_helper = Puppeteer::TimeoutHelper.new('Page.printToPDF',
1141
1462
  timeout_ms: options[:timeout],
@@ -1158,7 +1479,8 @@ class Puppeteer::Page
1158
1479
  ).read_as_chunks
1159
1480
  end
1160
1481
 
1161
- # @return [String]
1482
+ # @rbs options: Hash[Symbol, untyped] -- PDF options
1483
+ # @rbs return: String -- PDF data
1162
1484
  def pdf(options = {})
1163
1485
  chunks = create_pdf_stream(options)
1164
1486
 
@@ -1186,65 +1508,79 @@ class Puppeteer::Page
1186
1508
  end
1187
1509
  end
1188
1510
 
1189
- # @param run_before_unload [Boolean]
1511
+ # @rbs run_before_unload: bool -- Whether to run beforeunload handlers
1512
+ # @rbs return: void -- No return value
1190
1513
  def close(run_before_unload: false)
1191
- unless @client.connection
1192
- raise 'Protocol error: Connection closed. Most likely the page has been closed.'
1193
- end
1514
+ guard = browser_context.wait_for_screenshot_operations
1515
+ begin
1516
+ unless @client.connection
1517
+ raise 'Protocol error: Connection closed. Most likely the page has been closed.'
1518
+ end
1194
1519
 
1195
- if run_before_unload
1196
- @client.send_message('Page.close')
1197
- else
1198
- @client.connection.send_message('Target.closeTarget', targetId: @target.target_id)
1199
- await @target.is_closed_promise
1520
+ if run_before_unload
1521
+ @client.send_message('Page.close')
1522
+ else
1523
+ @client.connection.send_message('Target.closeTarget', targetId: @target.target_id)
1524
+ @target.is_closed_promise.wait
1200
1525
 
1201
- # @closed sometimes remains false, so wait for @closed = true with 100ms timeout.
1202
- 25.times do
1203
- break if @closed
1204
- sleep 0.004
1526
+ # @closed sometimes remains false, so wait for @closed = true with 100ms timeout.
1527
+ 25.times do
1528
+ break if @closed
1529
+ Puppeteer::AsyncUtils.sleep_seconds(0.004)
1530
+ end
1205
1531
  end
1532
+ rescue Puppeteer::Connection::ProtocolError => err
1533
+ raise unless err.message.match?(/Target closed/i)
1534
+ ensure
1535
+ guard&.release
1206
1536
  end
1207
1537
  end
1208
1538
 
1209
- # @return [boolean]
1539
+ # @rbs return: bool -- Whether the page is closed
1210
1540
  def closed?
1211
1541
  @closed
1212
1542
  end
1213
1543
 
1214
1544
  attr_reader :mouse
1215
1545
 
1216
- # @param selector [String]
1217
- # @param delay [Number]
1218
- # @param button [String] "left"|"right"|"middle"
1219
- # @param click_count [Number]
1220
- def click(selector, delay: nil, button: nil, click_count: nil)
1221
- main_frame.click(selector, delay: delay, button: button, click_count: click_count)
1546
+ # @rbs selector: String -- CSS selector
1547
+ # @rbs delay: Numeric? -- Delay between down and up (ms)
1548
+ # @rbs button: String? -- Mouse button
1549
+ # @rbs click_count: Integer? -- Deprecated: use count (click_count only sets clickCount)
1550
+ # @rbs count: Integer? -- Number of clicks to perform
1551
+ # @rbs return: void -- No return value
1552
+ def click(selector, delay: nil, button: nil, click_count: nil, count: nil)
1553
+ main_frame.click(selector, delay: delay, button: button, click_count: click_count, count: count)
1222
1554
  end
1223
1555
 
1224
1556
  define_async_method :async_click
1225
1557
 
1226
- # @param {string} selector
1558
+ # @rbs selector: String -- CSS selector
1559
+ # @rbs return: void -- No return value
1227
1560
  def focus(selector)
1228
1561
  main_frame.focus(selector)
1229
1562
  end
1230
1563
 
1231
1564
  define_async_method :async_focus
1232
1565
 
1233
- # @param {string} selector
1566
+ # @rbs selector: String -- CSS selector
1567
+ # @rbs return: void -- No return value
1234
1568
  def hover(selector)
1235
1569
  main_frame.hover(selector)
1236
1570
  end
1237
1571
 
1238
- # @param {string} selector
1239
- # @param {!Array<string>} values
1240
- # @return {!Promise<!Array<string>>}
1572
+ # @rbs selector: String -- CSS selector
1573
+ # @rbs values: Array[String] -- Option values to select
1574
+ # @rbs return: Array[String] -- Selected values
1241
1575
  def select(selector, *values)
1242
1576
  main_frame.select(selector, *values)
1243
1577
  end
1244
1578
 
1245
1579
  define_async_method :async_select
1246
1580
 
1247
- # @param selector [String]
1581
+ # @rbs selector: String? -- CSS selector
1582
+ # @rbs block: Proc? -- Optional block for Object#tap usage
1583
+ # @rbs return: Puppeteer::Page | nil -- Page instance or nil
1248
1584
  def tap(selector: nil, &block)
1249
1585
  # resolves double meaning of tap.
1250
1586
  if selector.nil? && block
@@ -1253,54 +1589,60 @@ class Puppeteer::Page
1253
1589
  # browser.new_page.tap do |page|
1254
1590
  # ...
1255
1591
  # end
1256
- super(&block)
1257
- else
1258
- # Puppeteer's Page#tap.
1259
- main_frame.tap(selector)
1592
+ block.call(self)
1593
+ return self
1260
1594
  end
1595
+
1596
+ # Puppeteer's Page#tap.
1597
+ main_frame.tap(selector)
1598
+ nil
1261
1599
  end
1262
1600
 
1263
1601
  define_async_method :async_tap
1264
1602
 
1265
- # @param selector [String]
1266
- # @param text [String]
1267
- # @param delay [Number]
1603
+ # @rbs selector: String -- CSS selector
1604
+ # @rbs text: String -- Text to type
1605
+ # @rbs delay: Numeric? -- Delay between key presses (ms)
1606
+ # @rbs return: void -- No return value
1268
1607
  def type_text(selector, text, delay: nil)
1269
1608
  main_frame.type_text(selector, text, delay: delay)
1270
1609
  end
1271
1610
 
1272
1611
  define_async_method :async_type_text
1273
1612
 
1274
- # @param selector [String]
1275
- # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
1276
- # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
1277
- # @param timeout [Integer]
1613
+ # @rbs selector: String -- CSS selector
1614
+ # @rbs visible: bool? -- Wait for element to be visible
1615
+ # @rbs hidden: bool? -- Wait for element to be hidden
1616
+ # @rbs timeout: Numeric? -- Maximum wait time in milliseconds
1617
+ # @rbs return: Puppeteer::ElementHandle? -- Matching element or nil
1278
1618
  def wait_for_selector(selector, visible: nil, hidden: nil, timeout: nil)
1279
1619
  main_frame.wait_for_selector(selector, visible: visible, hidden: hidden, timeout: timeout)
1280
1620
  end
1281
1621
 
1282
1622
  define_async_method :async_wait_for_selector
1283
1623
 
1284
- # @param milliseconds [Integer] the number of milliseconds to wait.
1624
+ # @rbs milliseconds: Numeric -- Time to wait in milliseconds
1625
+ # @rbs return: void -- No return value
1285
1626
  def wait_for_timeout(milliseconds)
1286
1627
  main_frame.wait_for_timeout(milliseconds)
1287
1628
  end
1288
1629
 
1289
- # @param xpath [String]
1290
- # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
1291
- # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
1292
- # @param timeout [Integer]
1630
+ # @rbs xpath: String -- XPath expression
1631
+ # @rbs visible: bool? -- Wait for element to be visible
1632
+ # @rbs hidden: bool? -- Wait for element to be hidden
1633
+ # @rbs timeout: Numeric? -- Maximum wait time in milliseconds
1634
+ # @rbs return: Puppeteer::ElementHandle? -- Matching element or nil
1293
1635
  def wait_for_xpath(xpath, visible: nil, hidden: nil, timeout: nil)
1294
1636
  main_frame.wait_for_xpath(xpath, visible: visible, hidden: hidden, timeout: timeout)
1295
1637
  end
1296
1638
 
1297
1639
  define_async_method :async_wait_for_xpath
1298
1640
 
1299
- # @param page_function [String]
1300
- # @param args [Integer|Array]
1301
- # @param polling [String]
1302
- # @param timeout [Integer]
1303
- # @return [Puppeteer::JSHandle]
1641
+ # @rbs page_function: String -- Function or expression to evaluate
1642
+ # @rbs args: Array[untyped] -- Arguments for evaluation
1643
+ # @rbs polling: String | Numeric | nil -- Polling strategy
1644
+ # @rbs timeout: Numeric? -- Maximum wait time in milliseconds
1645
+ # @rbs return: Puppeteer::JSHandle -- Handle to evaluation result
1304
1646
  def wait_for_function(page_function, args: [], polling: nil, timeout: nil)
1305
1647
  main_frame.wait_for_function(page_function, args: args, polling: polling, timeout: timeout)
1306
1648
  end