puppeteer-ruby 0.37.3 → 0.40.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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