puppeteer-ruby 0.37.3 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,122 @@
1
+ # Helper class to track network events by request ID
2
+ class Puppeteer::NetworkEventManager
3
+ def initialize
4
+ #
5
+ # There are four possible orders of events:
6
+ # A. `_onRequestWillBeSent`
7
+ # B. `_onRequestWillBeSent`, `_onRequestPaused`
8
+ # C. `_onRequestPaused`, `_onRequestWillBeSent`
9
+ # D. `_onRequestPaused`, `_onRequestWillBeSent`, `_onRequestPaused`,
10
+ # `_onRequestWillBeSent`, `_onRequestPaused`, `_onRequestPaused`
11
+ # (see crbug.com/1196004)
12
+ #
13
+ # For `_onRequest` we need the event from `_onRequestWillBeSent` and
14
+ # optionally the `interceptionId` from `_onRequestPaused`.
15
+ #
16
+ # If request interception is disabled, call `_onRequest` once per call to
17
+ # `_onRequestWillBeSent`.
18
+ # If request interception is enabled, call `_onRequest` once per call to
19
+ # `_onRequestPaused` (once per `interceptionId`).
20
+ #
21
+ # Events are stored to allow for subsequent events to call `_onRequest`.
22
+ #
23
+ # Note that (chains of) redirect requests have the same `requestId` (!) as
24
+ # the original request. We have to anticipate series of events like these:
25
+ # A. `_onRequestWillBeSent`,
26
+ # `_onRequestWillBeSent`, ...
27
+ # B. `_onRequestWillBeSent`, `_onRequestPaused`,
28
+ # `_onRequestWillBeSent`, `_onRequestPaused`, ...
29
+ # C. `_onRequestWillBeSent`, `_onRequestPaused`,
30
+ # `_onRequestPaused`, `_onRequestWillBeSent`, ...
31
+ # D. `_onRequestPaused`, `_onRequestWillBeSent`,
32
+ # `_onRequestPaused`, `_onRequestWillBeSent`, `_onRequestPaused`,
33
+ # `_onRequestWillBeSent`, `_onRequestPaused`, `_onRequestPaused`, ...
34
+ # (see crbug.com/1196004)
35
+ @request_will_be_sent_map = {}
36
+ @request_paused_map = {}
37
+ @http_requests_map = {}
38
+ #
39
+ # The below maps are used to reconcile Network.responseReceivedExtraInfo
40
+ # events with their corresponding request. Each response and redirect
41
+ # response gets an ExtraInfo event, and we don't know which will come first.
42
+ # This means that we have to store a Response or an ExtraInfo for each
43
+ # response, and emit the event when we get both of them. In addition, to
44
+ # handle redirects, we have to make them Arrays to represent the chain of
45
+ # events.
46
+ @response_received_extra_info_map = {}
47
+ @queued_redirect_info_map = {}
48
+ @queued_event_group_map = {}
49
+ end
50
+
51
+ def forget(network_request_id)
52
+ @request_will_be_sent_map.delete(network_request_id)
53
+ @request_paused_map.delete(network_request_id)
54
+ @queued_event_group_map.delete(network_request_id)
55
+ @queued_redirect_info_map.delete(network_request_id)
56
+ @response_received_extra_info_map.delete(network_request_id)
57
+ end
58
+
59
+ def response_extra_info(network_request_id)
60
+ @response_received_extra_info_map[network_request_id] ||= []
61
+ end
62
+
63
+ private def queued_redirect_info(fetch_request_id)
64
+ @queued_redirect_info_map[fetch_request_id] ||= []
65
+ end
66
+
67
+ def enqueue_redirect_info(fetch_request_id, redirect_info)
68
+ queued_redirect_info(fetch_request_id) << redirect_info
69
+ end
70
+
71
+ def take_queued_redirect_info(fetch_request_id)
72
+ queued_redirect_info(fetch_request_id).shift
73
+ end
74
+
75
+ def num_requests_in_progress
76
+ @http_requests_map.count { |_, request| !request.response }
77
+ end
78
+
79
+ def store_request_will_be_sent(network_request_id, event)
80
+ @request_will_be_sent_map[network_request_id] = event
81
+ end
82
+
83
+ def get_request_will_be_sent(network_request_id)
84
+ @request_will_be_sent_map[network_request_id]
85
+ end
86
+
87
+ def forget_request_will_be_sent(network_request_id)
88
+ @request_will_be_sent_map.delete(network_request_id)
89
+ end
90
+
91
+ def store_request_paused(network_request_id, event)
92
+ @request_paused_map[network_request_id] = event
93
+ end
94
+
95
+ def get_request_paused(network_request_id)
96
+ @request_paused_map[network_request_id]
97
+ end
98
+
99
+ def fotget_request_paused(network_request_id)
100
+ @request_paused_map.delete(network_request_id)
101
+ end
102
+
103
+ def store_request(network_request_id, request)
104
+ @http_requests_map[network_request_id] = request
105
+ end
106
+
107
+ def get_request(network_request_id)
108
+ @http_requests_map[network_request_id]
109
+ end
110
+
111
+ def forget_request(network_request_id)
112
+ @http_requests_map.delete(network_request_id)
113
+ end
114
+
115
+ def enqueue_event_group(network_request_id, queued_event_group)
116
+ @queued_event_group_map[network_request_id] = queued_event_group
117
+ end
118
+
119
+ def get_queued_event_group(network_request_id)
120
+ @queued_event_group_map[network_request_id]
121
+ end
122
+ end
@@ -53,6 +53,22 @@ class Puppeteer::NetworkManager
53
53
  end
54
54
  end
55
55
 
56
+ class RedirectInfo
57
+ def initialize(event:, fetch_request_id:)
58
+ @event = event
59
+ @fetch_request_id = fetch_request_id
60
+ end
61
+ attr_reader :event, :fetch_request_id
62
+ end
63
+
64
+ class QueuedEventGroup
65
+ def initialize(response_received_event:)
66
+ @response_received_event = response_received_event
67
+ end
68
+ attr_reader :response_received_event
69
+ attr_accessor :loading_finished_event, :loading_failed_event
70
+ end
71
+
56
72
  # @param {!Puppeteer.CDPSession} client
57
73
  # @param {boolean} ignoreHTTPSErrors
58
74
  # @param {!Puppeteer.FrameManager} frameManager
@@ -60,12 +76,7 @@ class Puppeteer::NetworkManager
60
76
  @client = client
61
77
  @ignore_https_errors = ignore_https_errors
62
78
  @frame_manager = frame_manager
63
-
64
- # @type {!Map<string, !Request>}
65
- @request_id_to_request = {}
66
-
67
- # @type {!Map<string, !Protocol.Network.requestWillBeSentPayload>}
68
- @request_id_to_request_with_be_sent_event = {}
79
+ @network_event_manager = Puppeteer::NetworkEventManager.new
69
80
 
70
81
  @extra_http_headers = {}
71
82
 
@@ -73,7 +84,6 @@ class Puppeteer::NetworkManager
73
84
  @user_request_interception_enabled = false
74
85
  @protocol_request_interception_enabled = false
75
86
  @user_cache_disabled = false
76
- @request_id_to_interception_id = {}
77
87
  @internal_network_condition = InternalNetworkCondition.new(@client)
78
88
 
79
89
  @client.on_event('Fetch.requestPaused') do |event|
@@ -97,6 +107,9 @@ class Puppeteer::NetworkManager
97
107
  @client.on_event('Network.loadingFailed') do |event|
98
108
  handle_loading_failed(event)
99
109
  end
110
+ @client.on_event('Network.responseReceivedExtraInfo') do |event|
111
+ handle_response_received_extra_info(event)
112
+ end
100
113
  end
101
114
 
102
115
  def init
@@ -106,6 +119,14 @@ class Puppeteer::NetworkManager
106
119
  end
107
120
  end
108
121
 
122
+ def inspect
123
+ values = %i[network_event_manager].map do |sym|
124
+ value = instance_variable_get(:"@#{sym}")
125
+ "@#{sym}=#{value}"
126
+ end
127
+ "#<Puppeteer::HTTPRequest #{values.join(' ')}>"
128
+ end
129
+
109
130
  # @param username [String|NilClass]
110
131
  # @param password [String|NilClass]
111
132
  def authenticate(username:, password:)
@@ -131,6 +152,10 @@ class Puppeteer::NetworkManager
131
152
  @extra_http_headers.dup
132
153
  end
133
154
 
155
+ def num_requests_in_progress
156
+ @network_event_manager.num_requests_in_progress
157
+ end
158
+
134
159
  # @param value [TrueClass|FalseClass]
135
160
  def offline_mode=(value)
136
161
  @internal_network_condition.offline_mode=(value)
@@ -185,14 +210,17 @@ class Puppeteer::NetworkManager
185
210
 
186
211
  private def handle_request_will_be_sent(event)
187
212
  # Request interception doesn't happen for data URLs with Network Service.
188
- if @protocol_request_interception_enabled && !event['request']['url'].start_with?('data:')
189
- request_id = event['requestId']
190
- interception_id = @request_id_to_interception_id.delete(request_id)
191
- if interception_id
192
- handle_request(event, interception_id)
193
- else
194
- @request_id_to_request_with_be_sent_event[request_id] = event
213
+ if @user_request_interception_enabled && !event['request']['url'].start_with?('data:')
214
+ network_request_id = event['requestId']
215
+ @network_event_manager.store_request_will_be_sent(network_request_id, event)
216
+
217
+ # CDP may have sent a Fetch.requestPaused event already. Check for it.
218
+ if_present(@network_event_manager.get_request_paused(network_request_id)) do |request_paused_event|
219
+ fetch_request_id = request_paused_event['requestId']
220
+ handle_request(event, fetch_request_id)
221
+ @network_event_manager.forget_request_paused(network_request_id)
195
222
  end
223
+
196
224
  return
197
225
  end
198
226
  handle_request(event, nil)
@@ -233,26 +261,61 @@ class Puppeteer::NetworkManager
233
261
  end
234
262
  end
235
263
 
236
- request_id = event['networkId']
237
- interception_id = event['requestId']
238
- if request_id && (request_will_be_sent_event = @request_id_to_request_with_be_sent_event.delete(request_id))
239
- handle_request(request_will_be_sent_event, interception_id)
264
+ network_request_id = event['networkId']
265
+ fetch_request_id = event['requestId']
266
+ return unless network_request_id
267
+
268
+ request_will_be_sent_event = @network_event_manager.get_request_will_be_sent(network_request_id)
269
+
270
+ # redirect requests have the same `requestId`
271
+ if request_will_be_sent_event &&
272
+ (request_will_be_sent_event['request']['url'] != event['request']['url'] ||
273
+ request_will_be_sent_event['request']['method'] != event['request']['method'])
274
+
275
+ @network_event_manager.forget_request_will_be_sent(network_request_id)
276
+ request_will_be_sent_event = nil
277
+ end
278
+
279
+ if request_will_be_sent_event
280
+ handle_request(request_will_be_sent_event, fetch_request_id)
240
281
  else
241
- @request_id_to_interception_id[request_id] = interception_id
282
+ @network_event_manager.store_request_paused(network_request_id, event)
242
283
  end
243
284
  end
244
285
 
245
- private def handle_request(event, interception_id)
286
+ private def handle_request(event, fetch_request_id)
246
287
  redirect_chain = []
247
288
  if event['redirectResponse']
248
- if_present(@request_id_to_request[event['requestId']]) do |request|
249
- handle_request_redirect(request, event['redirectResponse'])
289
+ # We want to emit a response and requestfinished for the
290
+ # redirectResponse, but we can't do so unless we have a
291
+ # responseExtraInfo ready to pair it up with. If we don't have any
292
+ # responseExtraInfos saved in our queue, they we have to wait until
293
+ # the next one to emit response and requestfinished, *and* we should
294
+ # also wait to emit this Request too because it should come after the
295
+ # response/requestfinished.
296
+ redirect_response_extra_info = nil
297
+ if event['redirectHasExtraInfo']
298
+ redirect_response_extra_info = @network_event_manager.response_extra_info(event['requestId']).shift
299
+ unless redirect_response_extra_info
300
+ redirect_info = RedirectInfo.new(
301
+ event: event,
302
+ fetch_request_id: fetch_request_id,
303
+ )
304
+ @network_event_manager.enqueue_redirect_info(event['requestId'], redirect_info)
305
+ return
306
+ end
307
+ end
308
+
309
+ # If we connect late to the target, we could have missed the
310
+ # requestWillBeSent event.
311
+ if_present(@network_event_manager.get_request(event['requestId'])) do |request|
312
+ handle_request_redirect(request, event['redirectResponse'], redirect_response_extra_info)
250
313
  redirect_chain = request.internal.redirect_chain
251
314
  end
252
315
  end
253
316
  frame = if_present(event['frameId']) { |frame_id| @frame_manager.frame(frame_id) }
254
- request = Puppeteer::HTTPRequest.new(@client, frame, interception_id, @user_request_interception_enabled, event, redirect_chain)
255
- @request_id_to_request[event['requestId']] = request
317
+ request = Puppeteer::HTTPRequest.new(@client, frame, fetch_request_id, @user_request_interception_enabled, event, redirect_chain)
318
+ @network_event_manager.store_request(event['requestId'], request)
256
319
  emit_event(NetworkManagerEmittedEvents::Request, request)
257
320
  begin
258
321
  request.finalize_interceptions
@@ -262,55 +325,133 @@ class Puppeteer::NetworkManager
262
325
  end
263
326
 
264
327
  private def handle_request_served_from_cache(event)
265
- if_present(@request_id_to_request[event['requestId']]) do |request|
328
+ request = @network_event_manager.get_request(event['requestId'])
329
+ if request
266
330
  request.internal.from_memory_cache = true
267
331
  end
332
+ emit_event(NetworkManagerEmittedEvents::RequestServedFromCache, request)
268
333
  end
269
334
 
270
335
  # @param request [Puppeteer::HTTPRequest]
271
336
  # @param response_payload [Hash]
272
- private def handle_request_redirect(request, response_payload)
273
- response = Puppeteer::HTTPResponse.new(@client, request, response_payload)
337
+ private def handle_request_redirect(request, response_payload, extra_info)
338
+ response = Puppeteer::HTTPResponse.new(@client, request, response_payload, extra_info)
274
339
  request.internal.response = response
275
340
  request.internal.redirect_chain << request
276
341
  response.internal.body_loaded_promise.reject(Puppeteer::HTTPResponse::Redirected.new)
277
- @request_id_to_request.delete(request.internal.request_id)
278
- @attempted_authentications.delete(request.internal.interception_id)
342
+ forget_request(request, false)
279
343
  emit_event(NetworkManagerEmittedEvents::Response, response)
280
344
  emit_event(NetworkManagerEmittedEvents::RequestFinished, request)
281
345
  end
282
346
 
283
- # @param event [Hash]
284
- private def handle_response_received(event)
285
- request = @request_id_to_request[event['requestId']]
347
+ private def emit_response_event(response_received_event, extra_info)
348
+ request = @network_event_manager.get_request(response_received_event['requestId'])
286
349
  # FileUpload sends a response without a matching request.
287
350
  return unless request
288
351
 
289
- response = Puppeteer::HTTPResponse.new(@client, request, event['response'])
352
+ unless @network_event_manager.response_extra_info(response_received_event['requestId']).empty?
353
+ debug_puts("Unexpected extraInfo events for request #{response_received_event['requestId']}")
354
+ end
355
+
356
+ response = Puppeteer::HTTPResponse.new(@client, request, response_received_event['response'], extra_info)
290
357
  request.internal.response = response
291
358
  emit_event(NetworkManagerEmittedEvents::Response, response)
292
359
  end
293
360
 
361
+ # @param event [Hash]
362
+ private def handle_response_received(event)
363
+ request = @network_event_manager.get_request(event['requestId'])
364
+ extra_info = nil
365
+ if request && !request.internal.from_memory_cache? && event['hasExtraInfo']
366
+ extra_info = @network_event_manager.response_extra_info(event['requestId']).shift
367
+
368
+ unless extra_info
369
+ # Wait until we get the corresponding ExtraInfo event.
370
+ @network_event_manager.enqueue_event_group(event['requestId'], QueuedEventGroup.new(event))
371
+ return
372
+ end
373
+ end
374
+ emit_response_event(event, extra_info)
375
+ end
376
+
377
+ private def handle_response_received_extra_info(event)
378
+ # We may have skipped a redirect response/request pair due to waiting for
379
+ # this ExtraInfo event. If so, continue that work now that we have the
380
+ # request.
381
+ if_present(@network_event_manager.take_queued_redirect_info(event['requestId'])) do |redirect_info|
382
+ @network_event_manager.response_extra_info(event['requestId']) << event
383
+ handle_request(redirect_info.event, redirect_info)
384
+ return
385
+ end
386
+
387
+ # We may have skipped response and loading events because we didn't have
388
+ # this ExtraInfo event yet. If so, emit those events now.
389
+ if_present(@network_event_manager.get_queued_event_group(event['requestId'])) do |queued_events|
390
+ emit_response_event(queued_events.response_received_event, event)
391
+ if_present(queued_events.loading_finished_event) do |loading_finished_event|
392
+ emit_loading_finished(loading_finished_event)
393
+ end
394
+ if_present(queued_events.loading_failed_event) do |loading_failed_event|
395
+ emit_loading_failed(loading_failed_event)
396
+ end
397
+ return
398
+ end
399
+
400
+ # Wait until we get another event that can use this ExtraInfo event.
401
+ @network_event_manager.response_extra_info(event['requestId']) << event
402
+ end
403
+
404
+ private def forget_request(request, forget_events)
405
+ request_id = request.internal.request_id
406
+ interception_id = request.internal.interception_id
407
+
408
+ @network_event_manager.forget_request(request_id)
409
+ @attempted_authentications.delete(interception_id)
410
+ if forget_events
411
+ @network_event_manager.forget(request_id)
412
+ end
413
+ end
414
+
294
415
  private def handle_loading_finished(event)
295
- request = @request_id_to_request[event['requestId']]
416
+ # If the response event for this request is still waiting on a
417
+ # corresponding ExtraInfo event, then wait to emit this event too.
418
+ queued_events = @network_event_manager.get_queued_event_group(event['requestId'])
419
+ if queued_events
420
+ queued_events.loading_finished_event = event
421
+ else
422
+ emit_loading_finished(event)
423
+ end
424
+ end
425
+
426
+ private def emit_loading_finished(event)
427
+ request = @network_event_manager.get_request(event['requestId'])
296
428
  # For certain requestIds we never receive requestWillBeSent event.
297
429
  # @see https://crbug.com/750469
298
430
  return unless request
299
431
 
300
-
301
432
  # Under certain conditions we never get the Network.responseReceived
302
433
  # event from protocol. @see https://crbug.com/883475
303
434
  if_present(request.response) do |response|
304
435
  response.internal.body_loaded_promise.fulfill(nil)
305
436
  end
306
437
 
307
- @request_id_to_request.delete(request.internal.request_id)
308
- @attempted_authentications.delete(request.internal.interception_id)
438
+ forget_request(request, true)
309
439
  emit_event(NetworkManagerEmittedEvents::RequestFinished, request)
310
440
  end
311
441
 
312
442
  private def handle_loading_failed(event)
313
- request = @request_id_to_request[event['requestId']]
443
+ # If the response event for this request is still waiting on a
444
+ # corresponding ExtraInfo event, then wait to emit this event too.
445
+ queued_events = @network_event_manager.get_queued_event_group(event['requestId'])
446
+ if queued_events
447
+ queued_events.loading_failed_event = event
448
+ else
449
+ emit_loading_failed(event)
450
+ end
451
+ end
452
+
453
+ private def emit_loading_failed(event)
454
+ request = @network_event_manager.get_request(event['requestId'])
314
455
  # For certain requestIds we never receive requestWillBeSent event.
315
456
  # @see https://crbug.com/750469
316
457
  return unless request
@@ -319,8 +460,7 @@ class Puppeteer::NetworkManager
319
460
  if_present(request.response) do |response|
320
461
  response.internal.body_loaded_promise.fulfill(nil)
321
462
  end
322
- @request_id_to_request.delete(request.internal.request_id)
323
- @attempted_authentications.delete(request.internal.interception_id)
463
+ forget_request(request, true)
324
464
  emit_event(NetworkManagerEmittedEvents::RequestFailed, request)
325
465
  end
326
466
  end
@@ -34,7 +34,7 @@ class Puppeteer::Page
34
34
  @type ||= 'png'
35
35
 
36
36
  if options[:quality]
37
- unless @type == 'jpeg'
37
+ if @type != 'jpeg' && @type != 'webp'
38
38
  raise ArgumentError.new("options.quality is unsupported for the #{@type} screenshots")
39
39
  end
40
40
  unless options[:quality].is_a?(Numeric)
@@ -50,19 +50,26 @@ class Puppeteer::Page
50
50
  @workers = {}
51
51
  @user_drag_interception_enabled = false
52
52
 
53
- @client.on_event('Target.attachedToTarget') do |event|
54
- if event['targetInfo']['type'] != 'worker'
53
+ @client.add_event_listener('Target.attachedToTarget') do |event|
54
+ if event['targetInfo']['type'] != 'worker' && event['targetInfo']['type'] != 'iframe'
55
55
  # If we don't detach from service workers, they will never die.
56
+ # We still want to attach to workers for emitting events.
57
+ # We still want to attach to iframes so sessions may interact with them.
58
+ # We detach from all other types out of an abundance of caution.
59
+ # See https://source.chromium.org/chromium/chromium/src/+/main:content/browser/devtools/devtools_agent_host_impl.cc?ss=chromium&q=f:devtools%20-f:out%20%22::kTypePage%5B%5D%22
60
+ # for the complete list of available types.
56
61
  @client.async_send_message('Target.detachFromTarget', sessionId: event['sessionId'])
57
62
  next
58
63
  end
59
64
 
60
- session = Puppeteer::Connection.from_session(@client).session(event['sessionId']) # rubocop:disable Lint/UselessAssignment
61
- # const worker = new Worker(session, event.targetInfo.url, this._addConsoleMessage.bind(this), this._handleException.bind(this));
62
- # this._workers.set(event.sessionId, worker);
63
- # this.emit(PageEmittedEvents::WorkerCreated, worker);
65
+ if event['targetInfo']['type'] == 'worker'
66
+ session = Puppeteer::Connection.from_session(@client).session(event['sessionId']) # rubocop:disable Lint/UselessAssignment
67
+ # const worker = new Worker(session, event.targetInfo.url, this._addConsoleMessage.bind(this), this._handleException.bind(this));
68
+ # this._workers.set(event.sessionId, worker);
69
+ # this.emit(PageEmittedEvents::WorkerCreated, worker);
70
+ end
64
71
  end
65
- @client.on_event('Target.detachedFromTarget') do |event|
72
+ @client.add_event_listener('Target.detachedFromTarget') do |event|
66
73
  session_id = event['sessionId']
67
74
  worker = @workers[session_id]
68
75
  next unless worker
@@ -103,10 +110,10 @@ class Puppeteer::Page
103
110
  @client.on_event('Page.loadEventFired') do |event|
104
111
  emit_event(PageEmittedEvents::Load)
105
112
  end
106
- @client.on('Runtime.consoleAPICalled') do |event|
113
+ @client.add_event_listener('Runtime.consoleAPICalled') do |event|
107
114
  handle_console_api(event)
108
115
  end
109
- @client.on('Runtime.bindingCalled') do |event|
116
+ @client.add_event_listener('Runtime.bindingCalled') do |event|
110
117
  handle_binding_called(event)
111
118
  end
112
119
  @client.on_event('Page.javascriptDialogOpening') do |event|
@@ -518,7 +525,7 @@ class Puppeteer::Page
518
525
  return
519
526
  end
520
527
 
521
- context = @frame_manager.execution_context_by_id(event['executionContextId'])
528
+ context = @frame_manager.execution_context_by_id(event['executionContextId'], @client)
522
529
  values = event['args'].map do |arg|
523
530
  remote_object = Puppeteer::RemoteObject.new(arg)
524
531
  Puppeteer::JSHandle.create(context: context, remote_object: remote_object)
@@ -664,19 +671,38 @@ class Puppeteer::Page
664
671
  private def wait_for_network_manager_event(event_name, predicate:, timeout:)
665
672
  option_timeout = timeout || @timeout_settings.timeout
666
673
 
667
- @wait_for_network_manager_event_listener_ids ||= {}
668
- if_present(@wait_for_network_manager_event_listener_ids[event_name]) do |listener_id|
674
+ promise = resolvable_future
675
+
676
+ listener_id = @frame_manager.network_manager.add_event_listener(event_name) do |event_target|
677
+ if predicate.call(event_target)
678
+ promise.fulfill(event_target)
679
+ end
680
+ end
681
+
682
+ begin
683
+ # Timeout.timeout(0) means "no limit" for timeout.
684
+ Timeout.timeout(option_timeout / 1000.0) do
685
+ await_any(promise, session_close_promise)
686
+ end
687
+ rescue Timeout::Error
688
+ raise Puppeteer::TimeoutError.new("waiting for #{event_name} failed: timeout #{option_timeout}ms exceeded")
689
+ ensure
669
690
  @frame_manager.network_manager.remove_event_listener(listener_id)
670
691
  end
692
+ end
693
+
694
+ private def wait_for_frame_manager_event(*event_names, predicate:, timeout:)
695
+ option_timeout = timeout || @timeout_settings.timeout
671
696
 
672
697
  promise = resolvable_future
673
698
 
674
- @wait_for_network_manager_event_listener_ids[event_name] =
675
- @frame_manager.network_manager.add_event_listener(event_name) do |event_target|
699
+ listener_ids = event_names.map do |event_name|
700
+ @frame_manager.add_event_listener(event_name) do |event_target|
676
701
  if predicate.call(event_target)
677
- promise.fulfill(event_target)
702
+ promise.fulfill(event_target) unless promise.resolved?
678
703
  end
679
704
  end
705
+ end
680
706
 
681
707
  begin
682
708
  # Timeout.timeout(0) means "no limit" for timeout.
@@ -684,9 +710,11 @@ class Puppeteer::Page
684
710
  await_any(promise, session_close_promise)
685
711
  end
686
712
  rescue Timeout::Error
687
- raise Puppeteer::TimeoutError.new("waiting for #{event_name} failed: timeout #{timeout}ms exceeded")
713
+ raise Puppeteer::TimeoutError.new("waiting for #{event_names.join(" or ")} failed: timeout #{option_timeout}ms exceeded")
688
714
  ensure
689
- @frame_manager.network_manager.remove_event_listener(@wait_for_network_manager_event_listener_ids[event_name])
715
+ listener_ids.each do |listener_id|
716
+ @frame_manager.remove_event_listener(listener_id)
717
+ end
690
718
  end
691
719
  end
692
720
 
@@ -709,7 +737,7 @@ class Puppeteer::Page
709
737
  if url
710
738
  -> (request) { request.url == url }
711
739
  else
712
- -> (request) { predicate.call(request) }
740
+ predicate
713
741
  end
714
742
 
715
743
  wait_for_network_manager_event(NetworkManagerEmittedEvents::Request,
@@ -743,7 +771,7 @@ class Puppeteer::Page
743
771
  if url
744
772
  -> (response) { response.url == url }
745
773
  else
746
- -> (response) { predicate.call(response) }
774
+ predicate
747
775
  end
748
776
 
749
777
  wait_for_network_manager_event(NetworkManagerEmittedEvents::Response,
@@ -758,6 +786,34 @@ class Puppeteer::Page
758
786
  # @param predicate [Proc(Puppeteer::HTTPRequest -> Boolean)]
759
787
  define_async_method :async_wait_for_response
760
788
 
789
+ def wait_for_frame(url: nil, predicate: nil, timeout: nil)
790
+ if !url && !predicate
791
+ raise ArgumentError.new('url or predicate must be specified')
792
+ end
793
+ if predicate && !predicate.is_a?(Proc)
794
+ raise ArgumentError.new('predicate must be a proc.')
795
+ end
796
+ frame_predicate =
797
+ if url
798
+ -> (frame) { frame.url == url }
799
+ else
800
+ predicate
801
+ end
802
+
803
+ wait_for_frame_manager_event(
804
+ FrameManagerEmittedEvents::FrameAttached,
805
+ FrameManagerEmittedEvents::FrameNavigated,
806
+ predicate: frame_predicate,
807
+ timeout: timeout,
808
+ )
809
+ end
810
+
811
+ # @!method async_wait_for_frame(url: nil, predicate: nil, timeout: nil)
812
+ #
813
+ # @param url [String]
814
+ # @param predicate [Proc(Puppeteer::Frame -> Boolean)]
815
+ define_async_method :async_wait_for_frame
816
+
761
817
  # @param timeout [number|nil]
762
818
  # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
763
819
  def go_back(timeout: nil, wait_until: nil)
@@ -44,6 +44,7 @@ class Puppeteer::Puppeteer
44
44
  args: nil,
45
45
  user_data_dir: nil,
46
46
  devtools: nil,
47
+ debugging_port: nil,
47
48
  headless: nil,
48
49
  ignore_https_errors: nil,
49
50
  default_viewport: NoViewport.new,
@@ -63,6 +64,7 @@ class Puppeteer::Puppeteer
63
64
  args: args,
64
65
  user_data_dir: user_data_dir,
65
66
  devtools: devtools,
67
+ debugging_port: debugging_port,
66
68
  headless: headless,
67
69
  ignore_https_errors: ignore_https_errors,
68
70
  default_viewport: default_viewport,
@@ -26,8 +26,8 @@ class Puppeteer::QueryHandlerManager
26
26
  @query_handler.query_one(element_handle, @selector)
27
27
  end
28
28
 
29
- def wait_for(dom_world, visible:, hidden:, timeout:)
30
- @query_handler.wait_for(dom_world, @selector, visible: visible, hidden: hidden, timeout: timeout)
29
+ def wait_for(dom_world, visible:, hidden:, timeout:, root:)
30
+ @query_handler.wait_for(dom_world, @selector, visible: visible, hidden: hidden, timeout: timeout, root: root)
31
31
  end
32
32
 
33
33
  def query_all(element_handle)
@@ -119,6 +119,11 @@ class Puppeteer::Target
119
119
  end
120
120
  end
121
121
 
122
+ # @internal
123
+ def raw_type
124
+ @target_info.type
125
+ end
126
+
122
127
  # @return {!Puppeteer.Browser}
123
128
  def browser
124
129
  @browser_context.browser
@@ -1,3 +1,3 @@
1
1
  module Puppeteer
2
- VERSION = '0.37.3'
2
+ VERSION = '0.40.0'
3
3
  end