puppeteer-ruby 0.39.0 → 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.
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