puppeteer-ruby 0.45.6 → 0.50.0.alpha6

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 (104) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -3
  3. data/AGENTS.md +170 -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 +605 -0
  9. data/CLAUDE/rbs_type_checking.md +101 -0
  10. data/CLAUDE/spec_migration_plans.md +1039 -0
  11. data/CLAUDE/testing.md +278 -0
  12. data/CLAUDE.md +242 -0
  13. data/README.md +9 -0
  14. data/Rakefile +7 -0
  15. data/Steepfile +28 -0
  16. data/docs/api_coverage.md +106 -57
  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 +236 -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 +177 -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 +77 -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/locators.rb +733 -0
  54. data/lib/puppeteer/mouse.rb +273 -64
  55. data/lib/puppeteer/network_event_manager.rb +7 -0
  56. data/lib/puppeteer/network_manager.rb +393 -112
  57. data/lib/puppeteer/p_query_handler.rb +367 -0
  58. data/lib/puppeteer/p_selector_parser.rb +241 -0
  59. data/lib/puppeteer/page/screenshot_task_queue.rb +14 -4
  60. data/lib/puppeteer/page.rb +583 -226
  61. data/lib/puppeteer/puppeteer.rb +171 -64
  62. data/lib/puppeteer/query_handler_manager.rb +66 -16
  63. data/lib/puppeteer/reactor_runner.rb +247 -0
  64. data/lib/puppeteer/remote_object.rb +127 -47
  65. data/lib/puppeteer/target.rb +74 -27
  66. data/lib/puppeteer/task_manager.rb +3 -1
  67. data/lib/puppeteer/timeout_helper.rb +6 -10
  68. data/lib/puppeteer/touch_handle.rb +39 -0
  69. data/lib/puppeteer/touch_screen.rb +72 -22
  70. data/lib/puppeteer/tracing.rb +3 -3
  71. data/lib/puppeteer/version.rb +1 -1
  72. data/lib/puppeteer/wait_task.rb +264 -101
  73. data/lib/puppeteer/web_socket.rb +2 -2
  74. data/lib/puppeteer/web_socket_transport.rb +91 -27
  75. data/lib/puppeteer/web_worker.rb +175 -0
  76. data/lib/puppeteer.rb +23 -4
  77. data/puppeteer-ruby.gemspec +15 -11
  78. data/sig/_external.rbs +8 -0
  79. data/sig/_supplementary.rbs +314 -0
  80. data/sig/puppeteer/browser.rbs +166 -0
  81. data/sig/puppeteer/cdp_session.rbs +64 -0
  82. data/sig/puppeteer/dialog.rbs +41 -0
  83. data/sig/puppeteer/element_handle.rbs +308 -0
  84. data/sig/puppeteer/execution_context.rbs +87 -0
  85. data/sig/puppeteer/frame.rbs +233 -0
  86. data/sig/puppeteer/http_request.rbs +214 -0
  87. data/sig/puppeteer/http_response.rbs +89 -0
  88. data/sig/puppeteer/js_handle.rbs +64 -0
  89. data/sig/puppeteer/keyboard.rbs +40 -0
  90. data/sig/puppeteer/locators.rbs +222 -0
  91. data/sig/puppeteer/mouse.rbs +113 -0
  92. data/sig/puppeteer/p_query_handler.rbs +73 -0
  93. data/sig/puppeteer/p_selector_parser.rbs +31 -0
  94. data/sig/puppeteer/page.rbs +522 -0
  95. data/sig/puppeteer/puppeteer.rbs +98 -0
  96. data/sig/puppeteer/remote_object.rbs +78 -0
  97. data/sig/puppeteer/touch_handle.rbs +21 -0
  98. data/sig/puppeteer/touch_screen.rbs +35 -0
  99. data/sig/puppeteer/web_worker.rbs +83 -0
  100. metadata +122 -45
  101. data/CHANGELOG.md +0 -397
  102. data/lib/puppeteer/concurrent_ruby_utils.rb +0 -81
  103. data/lib/puppeteer/firefox_target_manager.rb +0 -157
  104. 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,94 @@ 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
 
462
+ # @rbs return: Numeric -- Default timeout in milliseconds
463
+ def default_timeout
464
+ @timeout_settings.timeout
465
+ end
466
+
467
+ # @rbs selector_or_function: String -- Selector or JS function
468
+ # @rbs return: Puppeteer::Locator -- Locator for selector or function
469
+ def locator(selector_or_function)
470
+ if Puppeteer::Locator.function_string?(selector_or_function)
471
+ Puppeteer::FunctionLocator.create(self, selector_or_function)
472
+ else
473
+ Puppeteer::NodeLocator.create(self, selector_or_function)
474
+ end
475
+ end
476
+
327
477
  # `$()` in JavaScript.
328
- # @param {string} selector
329
- # @return {!Promise<?Puppeteer.ElementHandle>}
478
+ # @rbs selector: String -- CSS selector
479
+ # @rbs return: Puppeteer::ElementHandle? -- Matching element or nil
330
480
  def query_selector(selector)
331
481
  main_frame.query_selector(selector)
332
482
  end
@@ -335,18 +485,19 @@ class Puppeteer::Page
335
485
  define_async_method :async_query_selector
336
486
 
337
487
  # `$$()` 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)
488
+ # @rbs selector: String -- CSS selector
489
+ # @rbs isolate: bool? -- Use isolated world for queries
490
+ # @rbs return: Array[Puppeteer::ElementHandle] -- Matching elements
491
+ def query_selector_all(selector, isolate: nil)
492
+ main_frame.query_selector_all(selector, isolate: isolate)
342
493
  end
343
494
  alias_method :SS, :query_selector_all
344
495
 
345
496
  define_async_method :async_query_selector_all
346
497
 
347
- # @param {Function|string} pageFunction
348
- # @param {!Array<*>} args
349
- # @return {!Promise<!Puppeteer.JSHandle>}
498
+ # @rbs page_function: String -- Function or expression to evaluate
499
+ # @rbs args: Array[untyped] -- Arguments for evaluation
500
+ # @rbs return: Puppeteer::JSHandle -- Handle to evaluation result
350
501
  def evaluate_handle(page_function, *args)
351
502
  context = main_frame.execution_context
352
503
  context.evaluate_handle(page_function, *args)
@@ -354,17 +505,18 @@ class Puppeteer::Page
354
505
 
355
506
  define_async_method :async_evaluate_handle
356
507
 
357
- # @param {!Puppeteer.JSHandle} prototypeHandle
358
- # @return {!Promise<!Puppeteer.JSHandle>}
508
+ # @rbs prototype_handle: Puppeteer::JSHandle -- Prototype handle
509
+ # @rbs return: Puppeteer::JSHandle -- Handle to query result
359
510
  def query_objects(prototype_handle)
360
511
  context = main_frame.execution_context
361
512
  context.query_objects(prototype_handle)
362
513
  end
363
514
 
364
515
  # `$eval()` in JavaScript.
365
- # @param selector [String]
366
- # @param page_function [String]
367
- # @return [Object]
516
+ # @rbs selector: String -- CSS selector
517
+ # @rbs page_function: String -- Function or expression to evaluate
518
+ # @rbs args: Array[untyped] -- Arguments for evaluation
519
+ # @rbs return: untyped -- Evaluation result
368
520
  def eval_on_selector(selector, page_function, *args)
369
521
  main_frame.eval_on_selector(selector, page_function, *args)
370
522
  end
@@ -373,9 +525,10 @@ class Puppeteer::Page
373
525
  define_async_method :async_eval_on_selector
374
526
 
375
527
  # `$$eval()` in JavaScript.
376
- # @param selector [String]
377
- # @param page_function [String]
378
- # @return [Object]
528
+ # @rbs selector: String -- CSS selector
529
+ # @rbs page_function: String -- Function or expression to evaluate
530
+ # @rbs args: Array[untyped] -- Arguments for evaluation
531
+ # @rbs return: untyped -- Evaluation result
379
532
  def eval_on_selector_all(selector, page_function, *args)
380
533
  main_frame.eval_on_selector_all(selector, page_function, *args)
381
534
  end
@@ -384,15 +537,16 @@ class Puppeteer::Page
384
537
  define_async_method :async_eval_on_selector_all
385
538
 
386
539
  # `$x()` in JavaScript. $ is not allowed to use as a method name in Ruby.
387
- # @param {string} expression
388
- # @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
540
+ # @rbs expression: String -- XPath expression
541
+ # @rbs return: Array[Puppeteer::ElementHandle] -- Matching elements
389
542
  def Sx(expression)
390
543
  main_frame.Sx(expression)
391
544
  end
392
545
 
393
546
  define_async_method :async_Sx
394
547
 
395
- # @return [Array<Hash>]
548
+ # @rbs urls: Array[String] -- URLs to fetch cookies for
549
+ # @rbs return: Array[Hash[String, untyped]] -- Cookies list
396
550
  def cookies(*urls)
397
551
  @client.send_message('Network.getCookies', urls: (urls.empty? ? [url] : urls))['cookies']
398
552
  end
@@ -406,6 +560,8 @@ class Puppeteer::Page
406
560
  raise ArgumentError.new("Each coookie must have #{requires.join(" and ")} attribute.")
407
561
  end
408
562
 
563
+ # @rbs cookies: Array[Hash[Symbol | String, untyped]] -- cookies parameter
564
+ # @rbs return: void -- No return value
409
565
  def delete_cookie(*cookies)
410
566
  assert_cookie_params(cookies, requires: %i(name))
411
567
 
@@ -417,6 +573,8 @@ class Puppeteer::Page
417
573
  end
418
574
  end
419
575
 
576
+ # @rbs cookies: Array[Hash[Symbol | String, untyped]] -- cookies parameter
577
+ # @rbs return: void -- No return value
420
578
  def set_cookie(*cookies)
421
579
  assert_cookie_params(cookies, requires: %i(name value))
422
580
 
@@ -434,24 +592,27 @@ class Puppeteer::Page
434
592
  end
435
593
  end
436
594
 
437
- # @param url [String?]
438
- # @param path [String?]
439
- # @param content [String?]
440
- # @param type [String?]
441
- # @param id [String?]
595
+ # @rbs url: String? -- Script URL
596
+ # @rbs path: String? -- Path to script file
597
+ # @rbs content: String? -- Script contents
598
+ # @rbs type: String? -- Script type
599
+ # @rbs id: String? -- Script element ID
600
+ # @rbs return: Puppeteer::ElementHandle -- Script element handle
442
601
  def add_script_tag(url: nil, path: nil, content: nil, type: nil, id: nil)
443
602
  main_frame.add_script_tag(url: url, path: path, content: content, type: type, id: id)
444
603
  end
445
604
 
446
- # @param url [String?]
447
- # @param path [String?]
448
- # @param content [String?]
605
+ # @rbs url: String? -- Stylesheet URL
606
+ # @rbs path: String? -- Path to stylesheet file
607
+ # @rbs content: String? -- Stylesheet contents
608
+ # @rbs return: Puppeteer::ElementHandle -- Style element handle
449
609
  def add_style_tag(url: nil, path: nil, content: nil)
450
610
  main_frame.add_style_tag(url: url, path: path, content: content)
451
611
  end
452
612
 
453
- # @param name [String]
454
- # @param puppeteer_function [Proc]
613
+ # @rbs name: String -- Binding name
614
+ # @rbs puppeteer_function: Proc -- Ruby callback
615
+ # @rbs return: void -- No return value
455
616
  def expose_function(name, puppeteer_function)
456
617
  if @page_bindings[name]
457
618
  raise ArgumentError.new("Failed to add page binding with name `#{name}` already exists!")
@@ -486,42 +647,79 @@ class Puppeteer::Page
486
647
 
487
648
  source = JavaScriptFunction.new(add_page_binding, ['exposedFun', name]).source
488
649
  @client.send_message('Runtime.addBinding', name: name)
489
- @client.send_message('Page.addScriptToEvaluateOnNewDocument', source: source)
650
+ script = @client.send_message('Page.addScriptToEvaluateOnNewDocument', source: source)
651
+ @page_binding_ids[name] = script['identifier']
490
652
 
491
653
  promises = @frame_manager.frames.map do |frame|
492
654
  frame.async_evaluate("() => #{source}")
493
655
  end
494
- await_all(*promises)
656
+ Puppeteer::AsyncUtils.await_promise_all(*promises)
495
657
 
496
658
  nil
497
659
  end
498
660
 
499
- # @param username [String?]
500
- # @param password [String?]
661
+ # @rbs name: String -- Binding name
662
+ # @rbs return: void -- No return value
663
+ def remove_exposed_function(name)
664
+ identifier = @page_binding_ids[name]
665
+ unless identifier
666
+ raise ArgumentError.new("Function with name \"#{name}\" does not exist")
667
+ end
668
+
669
+ @page_binding_ids.delete(name)
670
+ @page_bindings.delete(name)
671
+
672
+ @client.send_message('Runtime.removeBinding', name: name)
673
+ @client.send_message('Page.removeScriptToEvaluateOnNewDocument', identifier: identifier)
674
+
675
+ remove_script = '(name) => { delete window[name]; }'
676
+ @frame_manager.frames.each do |frame|
677
+ frame.evaluate(remove_script, name)
678
+ rescue StandardError
679
+ nil
680
+ end
681
+ nil
682
+ end
683
+
684
+ # @rbs username: String? -- HTTP basic auth username
685
+ # @rbs password: String? -- HTTP basic auth password
686
+ # @rbs return: void -- No return value
501
687
  def authenticate(username: nil, password: nil)
502
688
  @frame_manager.network_manager.authenticate(username: username, password: password)
503
689
  end
504
690
 
505
- # @param headers [Hash]
691
+ # @rbs headers: Hash[String, String] -- Extra HTTP headers
692
+ # @rbs return: void -- No return value
506
693
  def extra_http_headers=(headers)
507
694
  @frame_manager.network_manager.extra_http_headers = headers
508
695
  end
509
696
 
510
- # @param user_agent [String]
511
- # @param user_agent_metadata [Hash]
697
+ # @rbs user_agent: String -- User agent string
698
+ # @rbs user_agent_metadata: Hash[String, untyped]? -- User agent metadata
699
+ # @rbs return: void -- No return value
512
700
  def set_user_agent(user_agent, user_agent_metadata = nil)
513
701
  @frame_manager.network_manager.set_user_agent(user_agent, user_agent_metadata)
514
702
  end
515
703
  alias_method :user_agent=, :set_user_agent
516
704
 
705
+ # @rbs return: Puppeteer::Page::Metrics -- Page metrics
517
706
  def metrics
518
707
  response = @client.send_message('Performance.getMetrics')
519
708
  Metrics.new(response['metrics'])
520
709
  end
521
710
 
522
- class PageError < StandardError ; end
711
+ class PageError < Puppeteer::Error ; end
523
712
 
524
713
  private def handle_exception(exception_details)
714
+ exception = exception_details['exception']
715
+ if exception
716
+ is_error_object = exception['type'] == 'object' && exception['subtype'] == 'error'
717
+ if !is_error_object && !exception.key?('objectId')
718
+ emit_event(PageEmittedEvents::PageError, Puppeteer::RemoteObject.new(exception).value)
719
+ return
720
+ end
721
+ end
722
+
525
723
  message = Puppeteer::ExceptionDetails.new(exception_details).message
526
724
  err = PageError.new(message)
527
725
  # err.stack = ''; // Don't report clientside error with a node stack attached
@@ -554,6 +752,8 @@ class Puppeteer::Page
554
752
  add_console_message(event['type'], values, event['stackTrace'])
555
753
  end
556
754
 
755
+ # @rbs event: Hash[String, untyped] -- Binding called payload
756
+ # @rbs return: void -- No return value
557
757
  def handle_binding_called(event)
558
758
  execution_context_id = event['executionContextId']
559
759
  payload =
@@ -595,13 +795,18 @@ class Puppeteer::Page
595
795
  JavaScriptFunction.new(deliver_error, [name, seq, err.message]).source
596
796
  end
597
797
 
598
- @client.async_send_message('Runtime.evaluate', expression: expression, contextId: execution_context_id).rescue do |error|
798
+ Async do
799
+ @client.async_send_message('Runtime.evaluate', expression: expression, contextId: execution_context_id).wait
800
+ rescue => error
599
801
  debug_puts(error)
600
802
  end
601
803
  end
602
804
 
603
805
  private def add_console_message(type, args, stack_trace)
604
- text_tokens = args.map { |arg| arg.remote_object.value }
806
+ text_tokens = args.map do |arg|
807
+ value = arg.remote_object.value
808
+ value.nil? ? arg.to_s : value
809
+ end
605
810
 
606
811
  stack_trace_locations =
607
812
  if stack_trace && stack_trace['callFrames']
@@ -624,7 +829,7 @@ class Puppeteer::Page
624
829
  unless %w(alert confirm prompt beforeunload).include?(dialog_type)
625
830
  raise ArgumentError.new("Unknown javascript dialog type: #{dialog_type}")
626
831
  end
627
- dialog = Puppeteer::Dialog.new(@client,
832
+ dialog = Puppeteer::CdpDialog.new(@client,
628
833
  type: dialog_type,
629
834
  message: event['message'],
630
835
  default_value: event['defaultPrompt'])
@@ -641,72 +846,101 @@ class Puppeteer::Page
641
846
  @client.send_message('Emulation.setDefaultBackgroundColorOverride')
642
847
  end
643
848
 
644
- # @return [String]
849
+ # @rbs return: String? -- Page URL
645
850
  def url
646
851
  main_frame.url
647
852
  end
648
853
 
649
- # @return [String]
854
+ # @rbs return: String -- Page HTML content
650
855
  def content
651
856
  main_frame.content
652
857
  end
653
858
 
654
- # @param html [String]
655
- # @param timeout [Integer]
656
- # @param wait_until [String|Array<String>]
859
+ # @rbs html: String -- HTML content
860
+ # @rbs timeout: Numeric? -- Navigation timeout in milliseconds
861
+ # @rbs wait_until: String | Array[String] | nil -- Lifecycle events to wait for
862
+ # @rbs return: void -- No return value
657
863
  def set_content(html, timeout: nil, wait_until: nil)
658
864
  main_frame.set_content(html, timeout: timeout, wait_until: wait_until)
659
865
  end
660
866
 
661
- # @param html [String]
867
+ # @rbs html: String -- HTML content
868
+ # @rbs return: void -- No return value
662
869
  def content=(html)
663
870
  main_frame.set_content(html)
664
871
  end
665
872
 
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)
873
+ # @rbs url: String -- URL to navigate
874
+ # @rbs referer: String? -- Referer header value
875
+ # @rbs referer: String? -- Referer header value
876
+ # @rbs referrer_policy: String? -- Referrer policy
877
+ # @rbs timeout: Numeric? -- Navigation timeout in milliseconds
878
+ # @rbs wait_until: String | Array[String] | nil -- Lifecycle events to wait for
879
+ # @rbs return: Puppeteer::HTTPResponse? -- Navigation response
880
+ def goto(url, referer: nil, referrer_policy: nil, timeout: nil, wait_until: nil)
881
+ main_frame.goto(
882
+ url,
883
+ referer: referer,
884
+ referrer_policy: referrer_policy,
885
+ timeout: timeout,
886
+ wait_until: wait_until,
887
+ )
672
888
  end
673
889
 
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')
890
+ # @rbs timeout: Numeric? -- Navigation timeout in milliseconds
891
+ # @rbs wait_until: String | Array[String] | nil -- Lifecycle events to wait for
892
+ # @rbs ignore_cache: bool? -- Skip cache when reloading
893
+ # @rbs return: Puppeteer::HTTPResponse? -- Navigation response
894
+ def reload(timeout: nil, wait_until: nil, ignore_cache: nil)
895
+ params = {}
896
+ params[:ignoreCache] = ignore_cache unless ignore_cache.nil?
897
+
898
+ wait_for_navigation(timeout: timeout, wait_until: wait_until, ignore_same_document_navigation: true) do
899
+ if params.empty?
900
+ @client.send_message('Page.reload')
901
+ else
902
+ @client.send_message('Page.reload', **params)
903
+ end
680
904
  end
681
905
  end
682
906
 
683
- def wait_for_navigation(timeout: nil, wait_until: nil)
684
- main_frame.send(:wait_for_navigation, timeout: timeout, wait_until: wait_until)
907
+ # @rbs timeout: Numeric? -- Navigation timeout in milliseconds
908
+ # @rbs wait_until: String | Array[String] | nil -- Lifecycle events to wait for
909
+ # @rbs ignore_same_document_navigation: bool -- Ignore same-document navigation
910
+ # @rbs return: Puppeteer::HTTPResponse? -- Navigation response
911
+ def wait_for_navigation(timeout: nil, wait_until: nil, ignore_same_document_navigation: false)
912
+ main_frame.send(
913
+ :wait_for_navigation,
914
+ timeout: timeout,
915
+ wait_until: wait_until,
916
+ ignore_same_document_navigation: ignore_same_document_navigation,
917
+ )
685
918
  end
686
919
 
687
920
  # @!method async_wait_for_navigation(timeout: nil, wait_until: nil)
688
921
  #
689
- # @param timeout [number|nil]
690
- # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
691
922
  define_async_method :async_wait_for_navigation
692
923
 
693
924
  private def wait_for_network_manager_event(event_name, predicate:, timeout:)
694
925
  option_timeout = timeout || @timeout_settings.timeout
695
926
 
696
- promise = resolvable_future
927
+ promise = Async::Promise.new
697
928
 
698
929
  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)
930
+ if Puppeteer::AsyncUtils.await(predicate.call(event_target))
931
+ promise.resolve(event_target)
701
932
  end
702
933
  end
703
934
 
704
935
  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)
936
+ if option_timeout == 0
937
+ Puppeteer::AsyncUtils.await_promise_race(promise, session_close_promise)
938
+ else
939
+ Puppeteer::AsyncUtils.async_timeout(option_timeout, -> {
940
+ Puppeteer::AsyncUtils.await_promise_race(promise, session_close_promise)
941
+ }).wait
708
942
  end
709
- rescue Timeout::Error
943
+ rescue Async::TimeoutError
710
944
  raise Puppeteer::TimeoutError.new("waiting for #{event_name} failed: timeout #{option_timeout}ms exceeded")
711
945
  ensure
712
946
  @frame_manager.network_manager.remove_event_listener(listener_id)
@@ -716,22 +950,25 @@ class Puppeteer::Page
716
950
  private def wait_for_frame_manager_event(*event_names, predicate:, timeout:)
717
951
  option_timeout = timeout || @timeout_settings.timeout
718
952
 
719
- promise = resolvable_future
953
+ promise = Async::Promise.new
720
954
 
721
955
  listener_ids = event_names.map do |event_name|
722
956
  @frame_manager.add_event_listener(event_name) do |event_target|
723
- if predicate.call(event_target)
724
- promise.fulfill(event_target) unless promise.resolved?
957
+ if Puppeteer::AsyncUtils.await(predicate.call(event_target))
958
+ promise.resolve(event_target) unless promise.resolved?
725
959
  end
726
960
  end
727
961
  end
728
962
 
729
963
  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)
964
+ if option_timeout == 0
965
+ Puppeteer::AsyncUtils.await_promise_race(promise, session_close_promise)
966
+ else
967
+ Puppeteer::AsyncUtils.async_timeout(option_timeout, -> {
968
+ Puppeteer::AsyncUtils.await_promise_race(promise, session_close_promise)
969
+ }).wait
733
970
  end
734
- rescue Timeout::Error
971
+ rescue Async::TimeoutError
735
972
  raise Puppeteer::TimeoutError.new("waiting for #{event_names.join(" or ")} failed: timeout #{option_timeout}ms exceeded")
736
973
  ensure
737
974
  listener_ids.each do |listener_id|
@@ -741,13 +978,17 @@ class Puppeteer::Page
741
978
  end
742
979
 
743
980
  private def session_close_promise
744
- @disconnect_promise ||= resolvable_future do |future|
981
+ @disconnect_promise ||= Async::Promise.new.tap do |future|
745
982
  @client.observe_first(CDPSessionEmittedEvents::Disconnected) do
746
983
  future.reject(Puppeteer::CDPSession::Error.new('Target Closed'))
747
984
  end
748
985
  end
749
986
  end
750
987
 
988
+ # @rbs url: String? -- URL to match
989
+ # @rbs predicate: Proc? -- Predicate to match
990
+ # @rbs timeout: Numeric? -- Timeout in milliseconds
991
+ # @rbs return: Puppeteer::HTTPRequest -- Matching request
751
992
  def wait_for_request(url: nil, predicate: nil, timeout: nil)
752
993
  if !url && !predicate
753
994
  raise ArgumentError.new('url or predicate must be specified')
@@ -778,10 +1019,12 @@ class Puppeteer::Page
778
1019
  # Waits until request matches the given predicate
779
1020
  # wait_for_request(predicate: -> (req){ req.url.start_with?('https://example.com/search') })
780
1021
  #
781
- # @param url [String]
782
- # @param predicate [Proc(Puppeteer::HTTPRequest -> Boolean)]
783
1022
  define_async_method :async_wait_for_request
784
1023
 
1024
+ # @rbs url: String? -- URL to match
1025
+ # @rbs predicate: Proc? -- Predicate to match
1026
+ # @rbs timeout: Numeric? -- Timeout in milliseconds
1027
+ # @rbs return: Puppeteer::HTTPResponse -- Matching response
785
1028
  def wait_for_response(url: nil, predicate: nil, timeout: nil)
786
1029
  if !url && !predicate
787
1030
  raise ArgumentError.new('url or predicate must be specified')
@@ -804,10 +1047,68 @@ class Puppeteer::Page
804
1047
 
805
1048
  # @!method async_wait_for_response(url: nil, predicate: nil, timeout: nil)
806
1049
  #
807
- # @param url [String]
808
- # @param predicate [Proc(Puppeteer::HTTPRequest -> Boolean)]
809
1050
  define_async_method :async_wait_for_response
810
1051
 
1052
+ # @rbs idle_time: Numeric -- Idle time to wait for in milliseconds
1053
+ # @rbs timeout: Numeric? -- Timeout in milliseconds
1054
+ # @rbs concurrency: Integer -- Allowed number of concurrent requests
1055
+ # @rbs return: void -- No return value
1056
+ def wait_for_network_idle(idle_time: 500, timeout: nil, concurrency: 0)
1057
+ option_timeout = timeout || @timeout_settings.timeout
1058
+
1059
+ promise = Async::Promise.new
1060
+ idle_timer = nil
1061
+
1062
+ schedule_idle = lambda do
1063
+ return if @inflight_requests.size > concurrency
1064
+
1065
+ idle_timer&.stop
1066
+ idle_timer = Async do
1067
+ Puppeteer::AsyncUtils.sleep_seconds(idle_time / 1000.0)
1068
+ unless promise.resolved? || @inflight_requests.size > concurrency
1069
+ promise.resolve(nil)
1070
+ end
1071
+ end
1072
+ end
1073
+
1074
+ # Use raw listener to avoid request interception queue delaying idle tracking.
1075
+ request_listener = add_event_listener('request') do
1076
+ idle_timer&.stop
1077
+ idle_timer = nil
1078
+ end
1079
+ request_finished_listener = on('requestfinished') do
1080
+ schedule_idle.call
1081
+ end
1082
+ request_failed_listener = on('requestfailed') do
1083
+ schedule_idle.call
1084
+ end
1085
+
1086
+ schedule_idle.call
1087
+
1088
+ begin
1089
+ if option_timeout == 0
1090
+ Puppeteer::AsyncUtils.await_promise_race(promise, session_close_promise)
1091
+ else
1092
+ Puppeteer::AsyncUtils.async_timeout(option_timeout, -> {
1093
+ Puppeteer::AsyncUtils.await_promise_race(promise, session_close_promise)
1094
+ }).wait
1095
+ end
1096
+ rescue Async::TimeoutError
1097
+ raise Puppeteer::TimeoutError.new("waiting for network idle failed: timeout #{option_timeout}ms exceeded")
1098
+ ensure
1099
+ off(request_listener)
1100
+ off(request_finished_listener)
1101
+ off(request_failed_listener)
1102
+ idle_timer&.stop
1103
+ end
1104
+ end
1105
+
1106
+ define_async_method :async_wait_for_network_idle
1107
+
1108
+ # @rbs url: String? -- URL to match
1109
+ # @rbs predicate: Proc? -- Predicate to match
1110
+ # @rbs timeout: Numeric? -- Timeout in milliseconds
1111
+ # @rbs return: Puppeteer::Frame -- Matching frame
811
1112
  def wait_for_frame(url: nil, predicate: nil, timeout: nil)
812
1113
  if !url && !predicate
813
1114
  raise ArgumentError.new('url or predicate must be specified')
@@ -836,18 +1137,18 @@ class Puppeteer::Page
836
1137
 
837
1138
  # @!method async_wait_for_frame(url: nil, predicate: nil, timeout: nil)
838
1139
  #
839
- # @param url [String]
840
- # @param predicate [Proc(Puppeteer::Frame -> Boolean)]
841
1140
  define_async_method :async_wait_for_frame
842
1141
 
843
- # @param timeout [number|nil]
844
- # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
1142
+ # @rbs timeout: Numeric? -- Navigation timeout in milliseconds
1143
+ # @rbs wait_until: String | Array[String] | nil -- Lifecycle events to wait for
1144
+ # @rbs return: Puppeteer::HTTPResponse? -- Navigation response
845
1145
  def go_back(timeout: nil, wait_until: nil)
846
1146
  go(-1, timeout: timeout, wait_until: wait_until)
847
1147
  end
848
1148
 
849
- # @param timeout [number|nil]
850
- # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
1149
+ # @rbs timeout: Numeric? -- Navigation timeout in milliseconds
1150
+ # @rbs wait_until: String | Array[String] | nil -- Lifecycle events to wait for
1151
+ # @rbs return: Puppeteer::HTTPResponse? -- Navigation response
851
1152
  def go_forward(timeout: nil, wait_until: nil)
852
1153
  go(+1, timeout: timeout, wait_until: wait_until)
853
1154
  end
@@ -856,37 +1157,44 @@ class Puppeteer::Page
856
1157
  history = @client.send_message('Page.getNavigationHistory')
857
1158
  entries = history['entries']
858
1159
  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
1160
+ if index < 0 || index >= entries.length
1161
+ raise Puppeteer::Error.new('History entry to navigate to not found.')
1162
+ end
1163
+ entry = entries[index]
1164
+ wait_for_navigation(timeout: timeout, wait_until: wait_until) do
1165
+ @client.send_message('Page.navigateToHistoryEntry', entryId: entry['id'])
863
1166
  end
864
1167
  end
865
1168
 
866
1169
  # Brings page to front (activates tab).
1170
+ # @rbs return: void -- No return value
867
1171
  def bring_to_front
868
1172
  @client.send_message('Page.bringToFront')
869
1173
  end
870
1174
 
871
- # @param device [Device]
1175
+ # @rbs device: Puppeteer::Device -- Device descriptor
1176
+ # @rbs return: void -- No return value
872
1177
  def emulate(device)
873
1178
  self.viewport = device.viewport
874
1179
  self.user_agent = device.user_agent
875
1180
  end
876
1181
 
877
- # @param {boolean} enabled
1182
+ # @rbs enabled: bool -- Enable JavaScript
1183
+ # @rbs return: void -- No return value
878
1184
  def javascript_enabled=(enabled)
879
1185
  return if @javascript_enabled == enabled
880
1186
  @javascript_enabled = enabled
881
1187
  @client.send_message('Emulation.setScriptExecutionDisabled', value: !enabled)
882
1188
  end
883
1189
 
884
- # @param enabled [Boolean]
1190
+ # @rbs enabled: bool -- Enable bypassing CSP
1191
+ # @rbs return: void -- No return value
885
1192
  def bypass_csp=(enabled)
886
1193
  @client.send_message('Page.setBypassCSP', enabled: enabled)
887
1194
  end
888
1195
 
889
- # @param media_type [String|Symbol|nil] either of (media, print, nil)
1196
+ # @rbs media_type: (String | Symbol)? -- Media type override
1197
+ # @rbs return: void -- No return value
890
1198
  def emulate_media_type(media_type)
891
1199
  media_type_str = media_type.to_s
892
1200
  unless ['screen', 'print', ''].include?(media_type_str)
@@ -895,7 +1203,8 @@ class Puppeteer::Page
895
1203
  @client.send_message('Emulation.setEmulatedMedia', media: media_type_str)
896
1204
  end
897
1205
 
898
- # @param factor [Number|nil] Factor at which the CPU will be throttled (2x, 2.5x. 3x, ...). Passing `nil` disables cpu throttling.
1206
+ # @rbs factor: Numeric? -- CPU throttling rate
1207
+ # @rbs return: void -- No return value
899
1208
  def emulate_cpu_throttling(factor)
900
1209
  if factor.nil? || factor >= 1
901
1210
  @client.send_message('Emulation.setCPUThrottlingRate', rate: factor || 1)
@@ -904,7 +1213,8 @@ class Puppeteer::Page
904
1213
  end
905
1214
  end
906
1215
 
907
- # @param features [Array]
1216
+ # @rbs features: Array[Hash[Symbol, untyped]]? -- Media feature overrides
1217
+ # @rbs return: void -- No return value
908
1218
  def emulate_media_features(features)
909
1219
  if features.nil?
910
1220
  @client.send_message('Emulation.setEmulatedMedia', features: nil)
@@ -919,7 +1229,8 @@ class Puppeteer::Page
919
1229
  end
920
1230
  end
921
1231
 
922
- # @param timezone_id [String?]
1232
+ # @rbs timezone_id: String? -- Timezone ID
1233
+ # @rbs return: void -- No return value
923
1234
  def emulate_timezone(timezone_id)
924
1235
  @client.send_message('Emulation.setTimezoneOverride', timezoneId: timezone_id || '')
925
1236
  rescue => err
@@ -939,6 +1250,8 @@ class Puppeteer::Page
939
1250
  tritanopia
940
1251
  ].freeze
941
1252
 
1253
+ # @rbs vision_deficiency_type: String? -- Vision deficiency type
1254
+ # @rbs return: void -- No return value
942
1255
  def emulate_vision_deficiency(vision_deficiency_type)
943
1256
  value = vision_deficiency_type || 'none'
944
1257
  unless VISION_DEFICIENCY_TYPES.include?(value)
@@ -947,8 +1260,9 @@ class Puppeteer::Page
947
1260
  @client.send_message('Emulation.setEmulatedVisionDeficiency', type: value)
948
1261
  end
949
1262
 
950
- # @param is_user_active [Boolean]
951
- # @param is_screen_unlocked [Boolean]
1263
+ # @rbs is_user_active: bool? -- User activity override
1264
+ # @rbs is_screen_unlocked: bool? -- Screen unlocked override
1265
+ # @rbs return: void -- No return value
952
1266
  def emulate_idle_state(is_user_active: nil, is_screen_unlocked: nil)
953
1267
  overrides = {
954
1268
  isUserActive: is_user_active,
@@ -962,7 +1276,8 @@ class Puppeteer::Page
962
1276
  end
963
1277
  end
964
1278
 
965
- # @param viewport [Viewport]
1279
+ # @rbs viewport: Puppeteer::Viewport? -- Viewport settings
1280
+ # @rbs return: void -- No return value
966
1281
  def viewport=(viewport)
967
1282
  needs_reload = @emulation_manager.emulate_viewport(viewport)
968
1283
  @viewport = viewport
@@ -971,9 +1286,9 @@ class Puppeteer::Page
971
1286
 
972
1287
  attr_reader :viewport
973
1288
 
974
- # @param {Function|string} pageFunction
975
- # @param {!Array<*>} args
976
- # @return {!Promise<*>}
1289
+ # @rbs page_function: String -- page_function parameter
1290
+ # @rbs args: Array[untyped] -- args parameter
1291
+ # @rbs return: untyped -- Result
977
1292
  def evaluate(page_function, *args)
978
1293
  main_frame.evaluate(page_function, *args)
979
1294
  end
@@ -981,11 +1296,15 @@ class Puppeteer::Page
981
1296
  define_async_method :async_evaluate
982
1297
 
983
1298
  class JavaScriptFunction
1299
+ # @rbs expression: String -- Function expression
1300
+ # @rbs args: Array[untyped] -- Arguments for evaluation
1301
+ # @rbs return: void -- No return value
984
1302
  def initialize(expression, args)
985
1303
  @expression = expression
986
1304
  @args = args
987
1305
  end
988
1306
 
1307
+ # @rbs return: String -- Generated source
989
1308
  def source
990
1309
  "(#{@expression})(#{arguments})"
991
1310
  end
@@ -996,15 +1315,21 @@ class Puppeteer::Page
996
1315
  end
997
1316
 
998
1317
  class JavaScriptExpression
1318
+ # @rbs expression: String -- Expression to evaluate
1319
+ # @rbs return: void -- No return value
999
1320
  def initialize(expression)
1000
1321
  @expression = expression
1001
1322
  end
1002
1323
 
1324
+ # @rbs return: String -- Generated source
1003
1325
  def source
1004
1326
  @expression
1005
1327
  end
1006
1328
  end
1007
1329
 
1330
+ # @rbs page_function: String -- page_function parameter
1331
+ # @rbs args: Array[untyped] -- args parameter
1332
+ # @rbs return: Hash[String, untyped] -- CDP response
1008
1333
  def evaluate_on_new_document(page_function, *args)
1009
1334
  source =
1010
1335
  if ['=>', 'async', 'function'].any? { |keyword| page_function.include?(keyword) }
@@ -1016,23 +1341,32 @@ class Puppeteer::Page
1016
1341
  @client.send_message('Page.addScriptToEvaluateOnNewDocument', source: source)
1017
1342
  end
1018
1343
 
1019
- # @param {boolean} enabled
1344
+ # @rbs identifier: String -- Script identifier to remove
1345
+ # @rbs return: void
1346
+ def remove_script_to_evaluate_on_new_document(identifier)
1347
+ @client.send_message('Page.removeScriptToEvaluateOnNewDocument', identifier: identifier)
1348
+ end
1349
+
1350
+ # @rbs enabled: bool -- Enable cache usage
1020
1351
  def cache_enabled=(enabled)
1021
1352
  @frame_manager.network_manager.cache_enabled = enabled
1022
1353
  end
1023
1354
 
1024
- # @return [String]
1355
+ # @rbs return: String -- Page title
1025
1356
  def title
1026
1357
  main_frame.title
1027
1358
  end
1028
1359
 
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]
1360
+ # @rbs type: String? -- Image format
1361
+ # @rbs path: String? -- File path to save
1362
+ # @rbs full_page: bool? -- Capture full page
1363
+ # @rbs clip: Hash[Symbol, Numeric]? -- Clip rectangle
1364
+ # @rbs quality: Integer? -- JPEG quality
1365
+ # @rbs omit_background: bool? -- Omit background for PNG
1366
+ # @rbs encoding: String? -- Encoding (base64 or binary)
1367
+ # @rbs capture_beyond_viewport: bool? -- Capture beyond viewport
1368
+ # @rbs from_surface: bool? -- Capture from surface
1369
+ # @rbs return: String -- Screenshot data
1036
1370
  def screenshot(type: nil,
1037
1371
  path: nil,
1038
1372
  full_page: nil,
@@ -1055,14 +1389,14 @@ class Puppeteer::Page
1055
1389
  }.compact
1056
1390
  screenshot_options = ScreenshotOptions.new(options)
1057
1391
 
1392
+ guard = browser_context.start_screenshot
1058
1393
  @screenshot_task_queue.post_task do
1059
1394
  screenshot_task(screenshot_options.type, screenshot_options)
1060
1395
  end
1396
+ ensure
1397
+ guard&.release
1061
1398
  end
1062
1399
 
1063
- # @param {"png"|"jpeg"} format
1064
- # @param {!ScreenshotOptions=} options
1065
- # @return {!Promise<!Buffer|!String>}
1066
1400
  private def screenshot_task(format, screenshot_options)
1067
1401
  @client.send_message('Target.activateTarget', targetId: @target.target_id)
1068
1402
 
@@ -1129,13 +1463,15 @@ class Puppeteer::Page
1129
1463
  buffer
1130
1464
  end
1131
1465
 
1132
- class PrintToPdfIsNotImplementedError < StandardError
1466
+ class PrintToPdfIsNotImplementedError < Puppeteer::Error
1467
+ # @rbs return: void -- No return value
1133
1468
  def initialize
1134
1469
  super('pdf() is only available in headless mode. See https://github.com/puppeteer/puppeteer/issues/1829')
1135
1470
  end
1136
1471
  end
1137
1472
 
1138
- # @return [Enumerable<String>]
1473
+ # @rbs options: Hash[Symbol, untyped] -- PDF options
1474
+ # @rbs return: Enumerable[String] -- PDF data chunks
1139
1475
  def create_pdf_stream(options = {})
1140
1476
  timeout_helper = Puppeteer::TimeoutHelper.new('Page.printToPDF',
1141
1477
  timeout_ms: options[:timeout],
@@ -1158,7 +1494,8 @@ class Puppeteer::Page
1158
1494
  ).read_as_chunks
1159
1495
  end
1160
1496
 
1161
- # @return [String]
1497
+ # @rbs options: Hash[Symbol, untyped] -- PDF options
1498
+ # @rbs return: String -- PDF data
1162
1499
  def pdf(options = {})
1163
1500
  chunks = create_pdf_stream(options)
1164
1501
 
@@ -1186,65 +1523,79 @@ class Puppeteer::Page
1186
1523
  end
1187
1524
  end
1188
1525
 
1189
- # @param run_before_unload [Boolean]
1526
+ # @rbs run_before_unload: bool -- Whether to run beforeunload handlers
1527
+ # @rbs return: void -- No return value
1190
1528
  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
1529
+ guard = browser_context.wait_for_screenshot_operations
1530
+ begin
1531
+ unless @client.connection
1532
+ raise 'Protocol error: Connection closed. Most likely the page has been closed.'
1533
+ end
1194
1534
 
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
1535
+ if run_before_unload
1536
+ @client.send_message('Page.close')
1537
+ else
1538
+ @client.connection.send_message('Target.closeTarget', targetId: @target.target_id)
1539
+ @target.is_closed_promise.wait
1200
1540
 
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
1541
+ # @closed sometimes remains false, so wait for @closed = true with 100ms timeout.
1542
+ 25.times do
1543
+ break if @closed
1544
+ Puppeteer::AsyncUtils.sleep_seconds(0.004)
1545
+ end
1205
1546
  end
1547
+ rescue Puppeteer::Connection::ProtocolError => err
1548
+ raise unless err.message.match?(/Target closed/i)
1549
+ ensure
1550
+ guard&.release
1206
1551
  end
1207
1552
  end
1208
1553
 
1209
- # @return [boolean]
1554
+ # @rbs return: bool -- Whether the page is closed
1210
1555
  def closed?
1211
1556
  @closed
1212
1557
  end
1213
1558
 
1214
1559
  attr_reader :mouse
1215
1560
 
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)
1561
+ # @rbs selector: String -- CSS selector
1562
+ # @rbs delay: Numeric? -- Delay between down and up (ms)
1563
+ # @rbs button: String? -- Mouse button
1564
+ # @rbs click_count: Integer? -- Deprecated: use count (click_count only sets clickCount)
1565
+ # @rbs count: Integer? -- Number of clicks to perform
1566
+ # @rbs return: void -- No return value
1567
+ def click(selector, delay: nil, button: nil, click_count: nil, count: nil)
1568
+ main_frame.click(selector, delay: delay, button: button, click_count: click_count, count: count)
1222
1569
  end
1223
1570
 
1224
1571
  define_async_method :async_click
1225
1572
 
1226
- # @param {string} selector
1573
+ # @rbs selector: String -- CSS selector
1574
+ # @rbs return: void -- No return value
1227
1575
  def focus(selector)
1228
1576
  main_frame.focus(selector)
1229
1577
  end
1230
1578
 
1231
1579
  define_async_method :async_focus
1232
1580
 
1233
- # @param {string} selector
1581
+ # @rbs selector: String -- CSS selector
1582
+ # @rbs return: void -- No return value
1234
1583
  def hover(selector)
1235
1584
  main_frame.hover(selector)
1236
1585
  end
1237
1586
 
1238
- # @param {string} selector
1239
- # @param {!Array<string>} values
1240
- # @return {!Promise<!Array<string>>}
1587
+ # @rbs selector: String -- CSS selector
1588
+ # @rbs values: Array[String] -- Option values to select
1589
+ # @rbs return: Array[String] -- Selected values
1241
1590
  def select(selector, *values)
1242
1591
  main_frame.select(selector, *values)
1243
1592
  end
1244
1593
 
1245
1594
  define_async_method :async_select
1246
1595
 
1247
- # @param selector [String]
1596
+ # @rbs selector: String? -- CSS selector
1597
+ # @rbs block: Proc? -- Optional block for Object#tap usage
1598
+ # @rbs return: Puppeteer::Page | nil -- Page instance or nil
1248
1599
  def tap(selector: nil, &block)
1249
1600
  # resolves double meaning of tap.
1250
1601
  if selector.nil? && block
@@ -1253,54 +1604,60 @@ class Puppeteer::Page
1253
1604
  # browser.new_page.tap do |page|
1254
1605
  # ...
1255
1606
  # end
1256
- super(&block)
1257
- else
1258
- # Puppeteer's Page#tap.
1259
- main_frame.tap(selector)
1607
+ block.call(self)
1608
+ return self
1260
1609
  end
1610
+
1611
+ # Puppeteer's Page#tap.
1612
+ main_frame.tap(selector)
1613
+ nil
1261
1614
  end
1262
1615
 
1263
1616
  define_async_method :async_tap
1264
1617
 
1265
- # @param selector [String]
1266
- # @param text [String]
1267
- # @param delay [Number]
1618
+ # @rbs selector: String -- CSS selector
1619
+ # @rbs text: String -- Text to type
1620
+ # @rbs delay: Numeric? -- Delay between key presses (ms)
1621
+ # @rbs return: void -- No return value
1268
1622
  def type_text(selector, text, delay: nil)
1269
1623
  main_frame.type_text(selector, text, delay: delay)
1270
1624
  end
1271
1625
 
1272
1626
  define_async_method :async_type_text
1273
1627
 
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]
1628
+ # @rbs selector: String -- CSS selector
1629
+ # @rbs visible: bool? -- Wait for element to be visible
1630
+ # @rbs hidden: bool? -- Wait for element to be hidden
1631
+ # @rbs timeout: Numeric? -- Maximum wait time in milliseconds
1632
+ # @rbs return: Puppeteer::ElementHandle? -- Matching element or nil
1278
1633
  def wait_for_selector(selector, visible: nil, hidden: nil, timeout: nil)
1279
1634
  main_frame.wait_for_selector(selector, visible: visible, hidden: hidden, timeout: timeout)
1280
1635
  end
1281
1636
 
1282
1637
  define_async_method :async_wait_for_selector
1283
1638
 
1284
- # @param milliseconds [Integer] the number of milliseconds to wait.
1639
+ # @rbs milliseconds: Numeric -- Time to wait in milliseconds
1640
+ # @rbs return: void -- No return value
1285
1641
  def wait_for_timeout(milliseconds)
1286
1642
  main_frame.wait_for_timeout(milliseconds)
1287
1643
  end
1288
1644
 
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]
1645
+ # @rbs xpath: String -- XPath expression
1646
+ # @rbs visible: bool? -- Wait for element to be visible
1647
+ # @rbs hidden: bool? -- Wait for element to be hidden
1648
+ # @rbs timeout: Numeric? -- Maximum wait time in milliseconds
1649
+ # @rbs return: Puppeteer::ElementHandle? -- Matching element or nil
1293
1650
  def wait_for_xpath(xpath, visible: nil, hidden: nil, timeout: nil)
1294
1651
  main_frame.wait_for_xpath(xpath, visible: visible, hidden: hidden, timeout: timeout)
1295
1652
  end
1296
1653
 
1297
1654
  define_async_method :async_wait_for_xpath
1298
1655
 
1299
- # @param page_function [String]
1300
- # @param args [Integer|Array]
1301
- # @param polling [String]
1302
- # @param timeout [Integer]
1303
- # @return [Puppeteer::JSHandle]
1656
+ # @rbs page_function: String -- Function or expression to evaluate
1657
+ # @rbs args: Array[untyped] -- Arguments for evaluation
1658
+ # @rbs polling: String | Numeric | nil -- Polling strategy
1659
+ # @rbs timeout: Numeric? -- Maximum wait time in milliseconds
1660
+ # @rbs return: Puppeteer::JSHandle -- Handle to evaluation result
1304
1661
  def wait_for_function(page_function, args: [], polling: nil, timeout: nil)
1305
1662
  main_frame.wait_for_function(page_function, args: args, polling: polling, timeout: timeout)
1306
1663
  end