puppeteer-ruby 0.39.0 → 0.40.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e09022ffac5da45a7c40bca6b612c8a67afece662142675625794b15b89a044b
4
- data.tar.gz: 9af45888cfcfaff3bc368facbd1b5fb28572924ccf0c8d33b05ba310894638bb
3
+ metadata.gz: af966d033f8cff7c99c121805ffe7dde6f4d73cfe0fa9f34937b9eedbeafd9b8
4
+ data.tar.gz: 3578653749391fd953144e21160beb6baee5eb6463b43b8cecafcf3f3433bda7
5
5
  SHA512:
6
- metadata.gz: 580410458c1e0f2ac6dd8fb678fce42ed5f12ee6a1ff70b90600b6601b945af4342db53f6b98fe73ba249a9d85e3e8d64efb664534f7b1231265dbfbeb2f8e09
7
- data.tar.gz: adecd0293ec7b7ade882d9c331b794b4a14e2e52999c164880e7f4b0fa3d723e7bc6b44a238a5ad04c50ad0bfc9c3d5d33e60b50ea40904dc49af1506ce3e72e
6
+ metadata.gz: 9ae375c2763cd2fd597f72b0e82ccae84eba414cb4e1b7b3e0067c250c869020118f58fc1d74e0bfe0bb512318501fcedd5c1a25f3725de44a29e01393227765
7
+ data.tar.gz: 4dec50af05864ec0410e4d99bbec2cdccfbf06412eb21b3f923be4de892244510ba4a5d553dafc04b62d6de6381caf852238a9e971d2ba2d30c3009e78c9e048
data/docs/api_coverage.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # API coverages
2
- - Puppeteer version: v12.0.0
3
- - puppeteer-ruby version: 0.39.0
2
+ - Puppeteer version: v13.0.1
3
+ - puppeteer-ruby version: 0.40.0
4
4
 
5
5
  ## Puppeteer
6
6
 
@@ -36,10 +36,11 @@ class Puppeteer::AriaQueryHandler
36
36
  end
37
37
  end
38
38
 
39
- def wait_for(dom_world, selector, visible: nil, hidden: nil, timeout: nil)
39
+ def wait_for(dom_world, selector, visible: nil, hidden: nil, timeout: nil, root: nil)
40
+ # addHandlerToWorld
40
41
  binding_function = Puppeteer::DOMWorld::BindingFunction.new(
41
42
  name: 'ariaQuerySelector',
42
- proc: -> (sel) { query_one(dom_world.send(:document), sel) },
43
+ proc: -> (sel) { query_one(root || dom_world.send(:document), sel) },
43
44
  )
44
45
  dom_world.send(:wait_for_selector_in_page,
45
46
  '(_, selector) => globalThis.ariaQuerySelector(selector)',
@@ -47,7 +48,9 @@ class Puppeteer::AriaQueryHandler
47
48
  visible: visible,
48
49
  hidden: hidden,
49
50
  timeout: timeout,
50
- binding_function: binding_function)
51
+ binding_function: binding_function,
52
+ root: root,
53
+ )
51
54
  end
52
55
 
53
56
  def query_all(element, selector)
@@ -149,7 +149,7 @@ class Puppeteer::BrowserRunner
149
149
  # Attempt to remove temporary profile directory to avoid littering.
150
150
  begin
151
151
  if @using_temp_user_data_dir
152
- FileUtils.rm_rf(@temp_directory)
152
+ FileUtils.rm_rf(@user_data_dir)
153
153
  end
154
154
  rescue => err
155
155
  debug_puts(err)
@@ -21,12 +21,12 @@ class Puppeteer::CustomQueryHandler
21
21
  nil
22
22
  end
23
23
 
24
- def wait_for(dom_world, selector, visible: nil, hidden: nil, timeout: nil)
24
+ def wait_for(dom_world, selector, visible: nil, hidden: nil, timeout: nil, root: nil)
25
25
  unless @query_one
26
26
  raise NotImplementedError.new("#{self.class}##{__method__} is not implemented.")
27
27
  end
28
28
 
29
- dom_world.send(:wait_for_selector_in_page, @query_one, selector, visible: visible, hidden: hidden, timeout: timeout)
29
+ dom_world.send(:wait_for_selector_in_page, @query_one, selector, visible: visible, hidden: hidden, timeout: timeout, root: root)
30
30
  end
31
31
 
32
32
  def query_all(element, selector)
@@ -420,10 +420,10 @@ class Puppeteer::DOMWorld
420
420
  # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
421
421
  # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
422
422
  # @param timeout [Integer]
423
- def wait_for_selector(selector, visible: nil, hidden: nil, timeout: nil)
423
+ def wait_for_selector(selector, visible: nil, hidden: nil, timeout: nil, root: nil)
424
424
  # call wait_for_selector_in_page with custom query selector.
425
425
  query_selector_manager = Puppeteer::QueryHandlerManager.instance
426
- query_selector_manager.detect_query_handler(selector).wait_for(self, visible: visible, hidden: hidden, timeout: timeout)
426
+ query_selector_manager.detect_query_handler(selector).wait_for(self, visible: visible, hidden: hidden, timeout: timeout, root: root)
427
427
  end
428
428
 
429
429
  private def binding_identifier(name, context)
@@ -497,10 +497,11 @@ class Puppeteer::DOMWorld
497
497
  # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
498
498
  # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
499
499
  # @param timeout [Integer]
500
- private def wait_for_selector_in_page(query_one, selector, visible: nil, hidden: nil, timeout: nil, binding_function: nil)
500
+ private def wait_for_selector_in_page(query_one, selector, visible: nil, hidden: nil, timeout: nil, root: nil, binding_function: nil)
501
501
  option_wait_for_visible = visible || false
502
502
  option_wait_for_hidden = hidden || false
503
503
  option_timeout = timeout || @timeout_settings.timeout
504
+ option_root = root
504
505
 
505
506
  polling =
506
507
  if option_wait_for_visible || option_wait_for_hidden
@@ -511,11 +512,11 @@ class Puppeteer::DOMWorld
511
512
  title = "selector #{selector}#{option_wait_for_hidden ? 'to be hidden' : ''}"
512
513
 
513
514
  selector_predicate = make_predicate_string(
514
- predicate_arg_def: '(selector, waitForVisible, waitForHidden)',
515
+ predicate_arg_def: '(root, selector, waitForVisible, waitForHidden)',
515
516
  predicate_query_handler: query_one,
516
517
  async: true,
517
518
  predicate_body: <<~JAVASCRIPT
518
- const node = await predicateQueryHandler(document, selector)
519
+ const node = await predicateQueryHandler(root, selector)
519
520
  return checkWaitForOptions(node, waitForVisible, waitForHidden);
520
521
  JAVASCRIPT
521
522
  )
@@ -527,6 +528,7 @@ class Puppeteer::DOMWorld
527
528
  polling: polling,
528
529
  timeout: option_timeout,
529
530
  args: [selector, option_wait_for_visible, option_wait_for_hidden],
531
+ root: option_root,
530
532
  binding_function: binding_function,
531
533
  )
532
534
  handle = wait_task.await_promise
@@ -541,10 +543,11 @@ class Puppeteer::DOMWorld
541
543
  # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
542
544
  # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
543
545
  # @param timeout [Integer]
544
- def wait_for_xpath(xpath, visible: nil, hidden: nil, timeout: nil)
546
+ def wait_for_xpath(xpath, visible: nil, hidden: nil, timeout: nil, root: nil)
545
547
  option_wait_for_visible = visible || false
546
548
  option_wait_for_hidden = hidden || false
547
549
  option_timeout = timeout || @timeout_settings.timeout
550
+ option_root = root
548
551
 
549
552
  polling =
550
553
  if option_wait_for_visible || option_wait_for_hidden
@@ -555,9 +558,9 @@ class Puppeteer::DOMWorld
555
558
  title = "XPath #{xpath}#{option_wait_for_hidden ? 'to be hidden' : ''}"
556
559
 
557
560
  xpath_predicate = make_predicate_string(
558
- predicate_arg_def: '(selector, waitForVisible, waitForHidden)',
561
+ predicate_arg_def: '(root, selector, waitForVisible, waitForHidden)',
559
562
  predicate_body: <<~JAVASCRIPT
560
- const node = document.evaluate(selector, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
563
+ const node = document.evaluate(selector, root, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
561
564
  return checkWaitForOptions(node, waitForVisible, waitForHidden);
562
565
  JAVASCRIPT
563
566
  )
@@ -569,6 +572,7 @@ class Puppeteer::DOMWorld
569
572
  polling: polling,
570
573
  timeout: option_timeout,
571
574
  args: [xpath, option_wait_for_visible, option_wait_for_hidden],
575
+ root: option_root,
572
576
  )
573
577
  handle = wait_task.await_promise
574
578
  unless handle.as_element
@@ -20,6 +20,60 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
20
20
  @disposed = false
21
21
  end
22
22
 
23
+ def inspect
24
+ values = %i[context remote_object page disposed].map do |sym|
25
+ value = instance_variable_get(:"@#{sym}")
26
+ "@#{sym}=#{value}"
27
+ end
28
+ "#<Puppeteer::ElementHandle #{values.join(' ')}>"
29
+ end
30
+
31
+ #
32
+ # Wait for the `selector` to appear within the element. If at the moment of calling the
33
+ # method the `selector` already exists, the method will return immediately. If
34
+ # the `selector` doesn't appear after the `timeout` milliseconds of waiting, the
35
+ # function will throw.
36
+ #
37
+ # This method does not work across navigations or if the element is detached from DOM.
38
+ #
39
+ # @param selector - A
40
+ # {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector}
41
+ # of an element to wait for
42
+ # @param options - Optional waiting parameters
43
+ # @returns Promise which resolves when element specified by selector string
44
+ # is added to DOM. Resolves to `null` if waiting for hidden: `true` and
45
+ # selector is not found in DOM.
46
+ # @remarks
47
+ # The optional parameters in `options` are:
48
+ #
49
+ # - `visible`: wait for the selected element to be present in DOM and to be
50
+ # visible, i.e. to not have `display: none` or `visibility: hidden` CSS
51
+ # properties. Defaults to `false`.
52
+ #
53
+ # - `hidden`: wait for the selected element to not be found in the DOM or to be hidden,
54
+ # i.e. have `display: none` or `visibility: hidden` CSS properties. Defaults to
55
+ # `false`.
56
+ #
57
+ # - `timeout`: maximum time to wait in milliseconds. Defaults to `30000`
58
+ # (30 seconds). Pass `0` to disable timeout. The default value can be changed
59
+ # by using the {@link Page.setDefaultTimeout} method.
60
+ def wait_for_selector(selector, visible: nil, hidden: nil, timeout: nil)
61
+ frame = @context.frame
62
+
63
+ secondary_world = frame.secondary_world
64
+ adopted_root = secondary_world.execution_context.adopt_element_handle(self)
65
+ handle = secondary_world.wait_for_selector(selector, visible: visible, hidden: hidden, timeout: timeout, root: adopted_root)
66
+ adopted_root.dispose
67
+ return nil unless handle
68
+
69
+ main_world = frame.main_world
70
+ result = main_world.execution_context.adopt_element_handle(handle)
71
+ handle.dispose
72
+ result
73
+ end
74
+
75
+ define_async_method :async_wait_for_selector
76
+
23
77
  def as_element
24
78
  self
25
79
  end
@@ -81,6 +81,7 @@ module NetworkManagerEmittedEvents ; end
81
81
 
82
82
  {
83
83
  Request: EventsDefinitionUtils.symbol('NetworkManager.Request'),
84
+ RequestServedFromCache: EventsDefinitionUtils.symbol('NetworkManager.RequestServedFromCache'),
84
85
  Response: EventsDefinitionUtils.symbol('NetworkManager.Response'),
85
86
  RequestFailed: EventsDefinitionUtils.symbol('NetworkManager.RequestFailed'),
86
87
  RequestFinished: EventsDefinitionUtils.symbol('NetworkManager.RequestFinished'),
@@ -31,7 +31,8 @@ class Puppeteer::HTTPResponse
31
31
  # @param client [Puppeteer::CDPSession]
32
32
  # @param request [Puppeteer::HTTPRequest]
33
33
  # @param response_payload [Hash]
34
- def initialize(client, request, response_payload)
34
+ # @param extra_info [Hash|nil]
35
+ def initialize(client, request, response_payload, extra_info)
35
36
  @client = client
36
37
  @request = request
37
38
 
@@ -41,14 +42,15 @@ class Puppeteer::HTTPResponse
41
42
  port: response_payload['remotePort'],
42
43
  )
43
44
 
44
- @status = response_payload['status']
45
- @status_text = response_payload['statusText']
45
+ @status_text = parse_štatus_text_from_extra_info(extra_info) || response_payload['statusText']
46
46
  @url = request.url
47
47
  @from_disk_cache = !!response_payload['fromDiskCache']
48
48
  @from_service_worker = !!response_payload['fromServiceWorker']
49
49
 
50
+ @status = extra_info ? extra_info['statusCode'] : response_payload['status']
50
51
  @headers = {}
51
- response_payload['headers'].each do |key, value|
52
+ headers = extra_info ? extra_info['headers'] : response_payload['headers']
53
+ headers.each do |key, value|
52
54
  @headers[key.downcase] = value
53
55
  end
54
56
  @security_details = if_present(response_payload['securityDetails']) do |security_payload|
@@ -62,6 +64,25 @@ class Puppeteer::HTTPResponse
62
64
 
63
65
  attr_reader :remote_address, :url, :status, :status_text, :headers, :security_details, :request
64
66
 
67
+ def inspect
68
+ values = %i[remote_address url status status_text headers security_details request].map do |sym|
69
+ value = instance_variable_get(:"@#{sym}")
70
+ "@#{sym}=#{value}"
71
+ end
72
+ "#<Puppeteer::HTTPRequest #{values.join(' ')}>"
73
+ end
74
+
75
+ private def parse_štatus_text_from_extra_info(extra_info)
76
+ return nil if !extra_info || !extra_info['headersText']
77
+ first_line = extra_info['headersText'].split("\r").first
78
+ return nil unless first_line
79
+ /[^ ]* [^ ]* (.*)/.match(first_line) do |m|
80
+ return m[1]
81
+ end
82
+
83
+ nil
84
+ end
85
+
65
86
  # @return [Boolean]
66
87
  def ok?
67
88
  @status == 0 || (@status >= 200 && @status <= 299)
@@ -36,6 +36,14 @@ class Puppeteer::JSHandle
36
36
 
37
37
  attr_reader :context, :remote_object
38
38
 
39
+ def inspect
40
+ values = %i[context remote_object disposed].map do |sym|
41
+ value = instance_variable_get(:"@#{sym}")
42
+ "@#{sym}=#{value}"
43
+ end
44
+ "#<Puppeteer::JSHandle #{values.join(' ')}>"
45
+ end
46
+
39
47
  # @return [Puppeteer::ExecutionContext]
40
48
  def execution_context
41
49
  @context
@@ -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
@@ -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)
@@ -1,3 +1,3 @@
1
1
  module Puppeteer
2
- VERSION = '0.39.0'
2
+ VERSION = '0.40.0'
3
3
  end
@@ -9,7 +9,7 @@ class Puppeteer::WaitTask
9
9
  end
10
10
  end
11
11
 
12
- def initialize(dom_world:, predicate_body:, title:, polling:, timeout:, args: [], binding_function: nil)
12
+ def initialize(dom_world:, predicate_body:, title:, polling:, timeout:, args: [], binding_function: nil, root: nil)
13
13
  if polling.is_a?(String)
14
14
  if polling != 'raf' && polling != 'mutation'
15
15
  raise ArgumentError.new("Unknown polling option: #{polling}")
@@ -25,6 +25,7 @@ class Puppeteer::WaitTask
25
25
  @dom_world = dom_world
26
26
  @polling = polling
27
27
  @timeout = timeout
28
+ @root = root
28
29
  @predicate_body = "return (#{predicate_body})(...args);"
29
30
  @args = args
30
31
  @binding_function = binding_function
@@ -68,6 +69,7 @@ class Puppeteer::WaitTask
68
69
  begin
69
70
  success = context.evaluate_handle(
70
71
  WAIT_FOR_PREDICATE_PAGE_FUNCTION,
72
+ @root,
71
73
  @predicate_body,
72
74
  @polling,
73
75
  @timeout,
@@ -121,8 +123,9 @@ class Puppeteer::WaitTask
121
123
  private define_async_method :async_rerun
122
124
 
123
125
  WAIT_FOR_PREDICATE_PAGE_FUNCTION = <<~JAVASCRIPT
124
- async function _(predicateBody, polling, timeout, ...args) {
126
+ async function _(root, predicateBody, polling, timeout, ...args) {
125
127
  const predicate = new Function('...args', predicateBody);
128
+ root = root || document
126
129
  let timedOut = false;
127
130
  if (timeout)
128
131
  setTimeout(() => (timedOut = true), timeout);
@@ -136,7 +139,7 @@ class Puppeteer::WaitTask
136
139
  * @return {!Promise<*>}
137
140
  */
138
141
  async function pollMutation() {
139
- const success = await predicate(...args);
142
+ const success = await predicate(root, ...args);
140
143
  if (success) return Promise.resolve(success);
141
144
  let fulfill;
142
145
  const result = new Promise((x) => (fulfill = x));
@@ -145,13 +148,13 @@ class Puppeteer::WaitTask
145
148
  observer.disconnect();
146
149
  fulfill();
147
150
  }
148
- const success = await predicate(...args);
151
+ const success = await predicate(root, ...args);
149
152
  if (success) {
150
153
  observer.disconnect();
151
154
  fulfill(success);
152
155
  }
153
156
  });
154
- observer.observe(document, {
157
+ observer.observe(root, {
155
158
  childList: true,
156
159
  subtree: true,
157
160
  attributes: true,
@@ -168,7 +171,7 @@ class Puppeteer::WaitTask
168
171
  fulfill();
169
172
  return;
170
173
  }
171
- const success = await predicate(...args);
174
+ const success = await predicate(root, ...args);
172
175
  if (success) fulfill(success);
173
176
  else requestAnimationFrame(onRaf);
174
177
  }
@@ -183,7 +186,7 @@ class Puppeteer::WaitTask
183
186
  fulfill();
184
187
  return;
185
188
  }
186
- const success = await predicate(...args);
189
+ const success = await predicate(root, ...args);
187
190
  if (success) fulfill(success);
188
191
  else setTimeout(onTimeout, pollInterval);
189
192
  }
data/lib/puppeteer.rb CHANGED
@@ -47,6 +47,7 @@ require 'puppeteer/launcher'
47
47
  require 'puppeteer/lifecycle_watcher'
48
48
  require 'puppeteer/mouse'
49
49
  require 'puppeteer/network_conditions'
50
+ require 'puppeteer/network_event_manager'
50
51
  require 'puppeteer/network_manager'
51
52
  require 'puppeteer/page'
52
53
  require 'puppeteer/protocol_stream_reader'
@@ -24,7 +24,7 @@ Gem::Specification.new do |spec|
24
24
  spec.add_dependency 'concurrent-ruby', '~> 1.1.0'
25
25
  spec.add_dependency 'websocket-driver', '>= 0.6.0'
26
26
  spec.add_dependency 'mime-types', '>= 3.0'
27
- spec.add_development_dependency 'bundler', '~> 2.2.3'
27
+ spec.add_development_dependency 'bundler', '~> 2.3.4'
28
28
  spec.add_development_dependency 'chunky_png'
29
29
  spec.add_development_dependency 'dry-inflector'
30
30
  spec.add_development_dependency 'pry-byebug'
@@ -32,7 +32,7 @@ Gem::Specification.new do |spec|
32
32
  spec.add_development_dependency 'rollbar'
33
33
  spec.add_development_dependency 'rspec', '~> 3.10.0 '
34
34
  spec.add_development_dependency 'rspec_junit_formatter' # for CircleCI.
35
- spec.add_development_dependency 'rubocop', '~> 1.23.0'
35
+ spec.add_development_dependency 'rubocop', '~> 1.24.0'
36
36
  spec.add_development_dependency 'rubocop-rspec'
37
37
  spec.add_development_dependency 'sinatra'
38
38
  spec.add_development_dependency 'webrick'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: puppeteer-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.39.0
4
+ version: 0.40.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - YusukeIwaki
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-12-17 00:00:00.000000000 Z
11
+ date: 2022-01-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: 2.2.3
61
+ version: 2.3.4
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: 2.2.3
68
+ version: 2.3.4
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: chunky_png
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -170,14 +170,14 @@ dependencies:
170
170
  requirements:
171
171
  - - "~>"
172
172
  - !ruby/object:Gem::Version
173
- version: 1.23.0
173
+ version: 1.24.0
174
174
  type: :development
175
175
  prerelease: false
176
176
  version_requirements: !ruby/object:Gem::Requirement
177
177
  requirements:
178
178
  - - "~>"
179
179
  - !ruby/object:Gem::Version
180
- version: 1.23.0
180
+ version: 1.24.0
181
181
  - !ruby/object:Gem::Dependency
182
182
  name: rubocop-rspec
183
183
  requirement: !ruby/object:Gem::Requirement
@@ -306,6 +306,7 @@ files:
306
306
  - lib/puppeteer/mouse.rb
307
307
  - lib/puppeteer/network_condition.rb
308
308
  - lib/puppeteer/network_conditions.rb
309
+ - lib/puppeteer/network_event_manager.rb
309
310
  - lib/puppeteer/network_manager.rb
310
311
  - lib/puppeteer/page.rb
311
312
  - lib/puppeteer/page/metrics.rb
@@ -346,7 +347,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
346
347
  - !ruby/object:Gem::Version
347
348
  version: '0'
348
349
  requirements: []
349
- rubygems_version: 3.1.6
350
+ rubygems_version: 3.3.3
350
351
  signing_key:
351
352
  specification_version: 4
352
353
  summary: A ruby port of puppeteer