puppeteer-ruby 0.0.15 → 0.0.16

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: 9bff69308f1dc62011bd6278beb2883c5f16b42dfae0d138c8a442c0ed78cd93
4
- data.tar.gz: f336776cfc903db13391c11110650d8103e0eb291fa9a9d79366c9b0f9544b9e
3
+ metadata.gz: 38a6ae4190bd04cbacb863d04f9862ebc8a37c6811fbfd690426ba051d5ea5d4
4
+ data.tar.gz: 60b61e143e63ed5e42b0bb6f3d9e5c27262fd2f51064c185e491056a5081707d
5
5
  SHA512:
6
- metadata.gz: fe90923f1c31681ec8bc0c092a9ca59976d18f682a99491de57f764f76b35ce27482f47b08348adc84f0b09e215c8cfe788730cb53d776b4922d1e397509ab23
7
- data.tar.gz: ea4cb7f51c4944d082592c2a6fad51c69e8c3e6f356d6c5e6867cf9da3821249e346db7beee103e96a74f517ee929dcc1c865f79a07837a5f8c10efac357f748
6
+ metadata.gz: fc3ab652476ee319fab2d691173a0175d4b03d72346329f2fb855a142c3090693f1fb5cfa52abb4badbfbadc7d4eec5028a295f63fb7ddd3e362321564a96b7c
7
+ data.tar.gz: 6de2484b1b939c3cab1bfa48bb2225c54686f13e7901f3df086d8e5c7b18aa9fc62afb5fd9a785a3fbbf12d2aef50cde1d085f3564ce9afab996f80a39c64936
@@ -36,6 +36,8 @@ require 'puppeteer/mouse'
36
36
  require 'puppeteer/network_manager'
37
37
  require 'puppeteer/page'
38
38
  require 'puppeteer/remote_object'
39
+ require 'puppeteer/request'
40
+ require 'puppeteer/response'
39
41
  require 'puppeteer/target'
40
42
  require 'puppeteer/timeout_settings'
41
43
  require 'puppeteer/touch_screen'
@@ -208,28 +208,6 @@ class Puppeteer::Frame
208
208
 
209
209
  define_async_method :async_type_text
210
210
 
211
- # /**
212
- # * @param {(string|number|Function)} selectorOrFunctionOrTimeout
213
- # * @param {!Object=} options
214
- # * @param {!Array<*>} args
215
- # * @return {!Promise<?Puppeteer.JSHandle>}
216
- # */
217
- # waitFor(selectorOrFunctionOrTimeout, options = {}, ...args) {
218
- # const xPathPattern = '//';
219
-
220
- # if (helper.isString(selectorOrFunctionOrTimeout)) {
221
- # const string = /** @type {string} */ (selectorOrFunctionOrTimeout);
222
- # if (string.startsWith(xPathPattern))
223
- # return this.waitForXPath(string, options);
224
- # return this.waitForSelector(string, options);
225
- # }
226
- # if (helper.isNumber(selectorOrFunctionOrTimeout))
227
- # return new Promise(fulfill => setTimeout(fulfill, /** @type {number} */ (selectorOrFunctionOrTimeout)));
228
- # if (typeof selectorOrFunctionOrTimeout === 'function')
229
- # return this.waitForFunction(selectorOrFunctionOrTimeout, options, ...args);
230
- # return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout)));
231
- # }
232
-
233
211
  # @param selector [String]
234
212
  # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
235
213
  # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
@@ -1,5 +1,7 @@
1
1
  class Puppeteer::NetworkManager
2
+ include Puppeteer::DebugPrint
2
3
  include Puppeteer::EventCallbackable
4
+ include Puppeteer::IfPresent
3
5
 
4
6
  class Credentials
5
7
  # @param username [String|NilClass]
@@ -23,19 +25,39 @@ class Puppeteer::NetworkManager
23
25
  @request_id_to_request = {}
24
26
 
25
27
  # @type {!Map<string, !Protocol.Network.requestWillBeSentPayload>}
26
- @request_id_to_request_with_be_sent_event
28
+ @request_id_to_request_with_be_sent_event = {}
27
29
 
28
30
  @extra_http_headers = {}
29
31
 
30
32
  @offline = false
31
33
 
32
- # /** @type {!Set<string>} */
33
- # this._attemptedAuthentications = new Set();
34
+ @attempted_authentications = Set.new
34
35
  @user_request_interception_enabled = false
35
36
  @protocol_request_interception_enabled = false
36
37
  @user_cache_disabled = false
37
- # /** @type {!Map<string, string>} */
38
- # this._requestIdToInterceptionId = new Map();
38
+ @request_id_to_interception_id = {}
39
+
40
+ @client.on_event('Fetch.requestPaused') do |event|
41
+ handle_request_paused(event)
42
+ end
43
+ @client.on_event('Fetch.authRequired') do |event|
44
+ handle_auth_required(event)
45
+ end
46
+ @client.on_event('Network.requestWillBeSent') do |event|
47
+ handle_request_will_be_sent(event)
48
+ end
49
+ @client.on_event('Network.requestServedFromCache') do |event|
50
+ handle_request_served_from_cache(event)
51
+ end
52
+ @client.on_event('Network.responseReceived') do |event|
53
+ handle_response_received(event)
54
+ end
55
+ @client.on_event('Network.loadingFinished') do |event|
56
+ handle_loading_finished(event)
57
+ end
58
+ @client.on_event('Network.loadingFailed') do |event|
59
+ handle_loading_failed(event)
60
+ end
39
61
  end
40
62
 
41
63
  def init
@@ -119,4 +141,140 @@ class Puppeteer::NetworkManager
119
141
  cache_disabled = @user_cache_disabled || @protocol_request_interception_enabled
120
142
  @client.send_message('Network.setCacheDisabled', cacheDisabled: cache_disabled)
121
143
  end
144
+
145
+ private def handle_request_will_be_sent(event)
146
+ # Request interception doesn't happen for data URLs with Network Service.
147
+ if @protocol_request_interception_enabled && !event['request']['url'].start_with?('data:')
148
+ request_id = event['requestId']
149
+ interception_id = @request_id_to_interception_id.delete(request_id)
150
+ if interception_id
151
+ handle_request(event, interception_id)
152
+ else
153
+ @request_id_to_request_with_be_sent_event[request_id] = event
154
+ end
155
+ return
156
+ end
157
+ handle_request(event, nil)
158
+ end
159
+
160
+ private def handle_auth_required(event)
161
+ response = 'Default'
162
+ if @attempted_authentications.include?(event['requestId'])
163
+ response = 'CancelAuth'
164
+ elsif @credentials
165
+ response = 'ProvideCredentials'
166
+ @attempted_authentications << event['requestId']
167
+ end
168
+
169
+ username = @credentials&.username
170
+ password = @credentials&.password
171
+
172
+ begin
173
+ @client.send_message('Fetch.continueWithAuth',
174
+ requestId: event['requestId'],
175
+ authChallengeResponse: {
176
+ response: response,
177
+ username: username,
178
+ password: password,
179
+ },
180
+ )
181
+ rescue => err
182
+ debug_puts(err)
183
+ end
184
+ end
185
+
186
+ private def handle_request_paused(event)
187
+ if !@user_request_interception_enabled && @protocol_request_interception_enabled
188
+ begin
189
+ @client.send_message('Fetch.continueRequest', requestId: event['requestId'])
190
+ rescue => err
191
+ debug_puts(err)
192
+ end
193
+ end
194
+
195
+ request_id = event['networkId']
196
+ interception_id = event['requestId']
197
+ if request_id && (request_will_be_sent_event = @request_id_to_request_with_be_sent_event.delete(request_id))
198
+ handle_request(request_will_be_sent_event, interception_id)
199
+ else
200
+ @request_id_to_interception_id[request_id] = interception_id
201
+ end
202
+ end
203
+
204
+ private def handle_request(event, interception_id)
205
+ redirect_chain = []
206
+ if event['redirectResponse']
207
+ if_present(@request_id_to_request[event['requestId']]) do |request|
208
+ handle_request_redirect(request, event['redirectResponse'])
209
+ redirect_chain = request.internal.redirect_chain
210
+ end
211
+ end
212
+ frame = if_present(event['frameId']) { |frame_id| @frame_manager.frame(frame_id) }
213
+ request = Puppeteer::Request.new(@client, frame, interception_id, @user_request_interception_enabled, event, redirect_chain)
214
+ @request_id_to_request[event['requestId']] = request
215
+ emit_event 'Events.NetworkManager.Request', request
216
+ end
217
+
218
+ private def handle_request_served_from_cache(event)
219
+ if_present(@request_id_to_request[event['requestId']]) do |request|
220
+ request.internal.from_memory_cache = true
221
+ end
222
+ end
223
+
224
+ # @param request [Puppeteer::Request]
225
+ # @param response_payload [Hash]
226
+ private def handle_request_redirect(request, response_payload)
227
+ response = Puppeteer::Response.new(@client, request, response_payload)
228
+ request.internal.response = response
229
+ request.internal.redirect_chain << request
230
+ response.internal.body_loaded_promise.reject(Puppeteer::Response::Redirected.new)
231
+ @request_id_to_request.delete(request.internal.request_id)
232
+ @attempted_authentications.delete(request.internal.interception_id)
233
+ emit_event 'Events.NetworkManager.Response', response
234
+ emit_event 'Events.NetworkManager.RequestFinished', request
235
+ end
236
+
237
+ # @param event [Hash]
238
+ private def handle_response_received(event)
239
+ request = @request_id_to_request[event['requestId']]
240
+ # FileUpload sends a response without a matching request.
241
+ return unless request
242
+
243
+ response = Puppeteer::Response.new(@client, request, event['response'])
244
+ request.internal.response = response
245
+ emit_event 'Events.NetworkManager.Response', response
246
+ end
247
+
248
+ private def handle_loading_finished(event)
249
+ request = @request_id_to_request[event['requestId']]
250
+ # For certain requestIds we never receive requestWillBeSent event.
251
+ # @see https://crbug.com/750469
252
+ return unless request
253
+
254
+
255
+ # Under certain conditions we never get the Network.responseReceived
256
+ # event from protocol. @see https://crbug.com/883475
257
+ if_present(request.response) do |response|
258
+ response.internal.body_loaded_promise.fulfill(nil)
259
+ end
260
+
261
+ @request_id_to_request.delete(request.internal.request_id)
262
+ @attempted_authentications.delete(request.internal.interception_id)
263
+ emit_event 'Events.NetworkManager.RequestFinished', request
264
+ end
265
+
266
+ private def handle_loading_failed(event)
267
+ request = @request_id_to_request[event['requestId']]
268
+ # For certain requestIds we never receive requestWillBeSent event.
269
+ # @see https://crbug.com/750469
270
+ return unless request
271
+
272
+ request.internal.failure_text = event['errorText']
273
+ if_present(request.response) do |response|
274
+ response.internal.body_loaded_promise.fulfill(nil)
275
+ end
276
+ @request_id_to_request.delete(request.internal.request_id)
277
+ @attempted_authentications.delete(request.internal.interception_id)
278
+ emit_event 'Events.NetworkManager.RequestFailed', request
279
+ end
122
280
  end
@@ -689,41 +689,94 @@ class Puppeteer::Page
689
689
 
690
690
  define_async_method :async_wait_for_navigation
691
691
 
692
- # /**
693
- # * @param {(string|Function)} urlOrPredicate
694
- # * @param {!{timeout?: number}=} options
695
- # * @return {!Promise<!Puppeteer.Request>}
696
- # */
697
- # async waitForRequest(urlOrPredicate, options = {}) {
698
- # const {
699
- # timeout = this._timeoutSettings.timeout(),
700
- # } = options;
701
- # return helper.waitForEvent(this._frameManager.networkManager(), Events.NetworkManager.Request, request => {
702
- # if (helper.isString(urlOrPredicate))
703
- # return (urlOrPredicate === request.url());
704
- # if (typeof urlOrPredicate === 'function')
705
- # return !!(urlOrPredicate(request));
706
- # return false;
707
- # }, timeout, this._sessionClosePromise());
708
- # }
692
+ private def wait_for_network_manager_event(event_name, predicate:, timeout:)
693
+ option_timeout = timeout || @timeout_settings.timeout
709
694
 
710
- # /**
711
- # * @param {(string|Function)} urlOrPredicate
712
- # * @param {!{timeout?: number}=} options
713
- # * @return {!Promise<!Puppeteer.Response>}
714
- # */
715
- # async waitForResponse(urlOrPredicate, options = {}) {
716
- # const {
717
- # timeout = this._timeoutSettings.timeout(),
718
- # } = options;
719
- # return helper.waitForEvent(this._frameManager.networkManager(), Events.NetworkManager.Response, response => {
720
- # if (helper.isString(urlOrPredicate))
721
- # return (urlOrPredicate === response.url());
722
- # if (typeof urlOrPredicate === 'function')
723
- # return !!(urlOrPredicate(response));
724
- # return false;
725
- # }, timeout, this._sessionClosePromise());
726
- # }
695
+ @wait_for_network_manager_event_listener_ids ||= {}
696
+ if_present(@wait_for_network_manager_event_listener_ids[event_name]) do |listener_id|
697
+ @frame_manager.network_manager.remove_event_listener(listener_id)
698
+ end
699
+
700
+ promise = resolvable_future
701
+
702
+ @wait_for_network_manager_event_listener_ids[event_name] =
703
+ @frame_manager.network_manager.add_event_listener(event_name) do |event_target|
704
+ if predicate.call(event_target)
705
+ promise.fulfill(nil)
706
+ end
707
+ end
708
+
709
+ begin
710
+ Timeout.timeout(option_timeout / 1000.0) do
711
+ await_any(promise, session_close_promise)
712
+ end
713
+ rescue Timeout::Error
714
+ raise Puppeteer::TimeoutError.new("waiting for #{event_name} failed: timeout #{timeout}ms exceeded")
715
+ ensure
716
+ @frame_manager.network_manager.remove_event_listener(@wait_for_network_manager_event_listener_ids[event_name])
717
+ end
718
+ end
719
+
720
+ private def session_close_promise
721
+ @disconnect_promise ||= resolvable_future do |future|
722
+ @client.observe_first('Events.CDPSession.Disconnected') do
723
+ future.reject(Puppeteer::CDPSession::Error.new('Target Closed'))
724
+ end
725
+ end
726
+ end
727
+
728
+ # - Waits until request URL matches
729
+ # `wait_for_request(url: 'https://example.com/awesome')`
730
+ # - Waits until request matches the given predicate
731
+ # `wait_for_request(predicate: -> (req){ req.url.start_with?('https://example.com/search') })`
732
+ #
733
+ # @param url [String]
734
+ # @param predicate [Proc(Puppeteer::Request -> Boolean)]
735
+ private def wait_for_request(url: nil, predicate: nil, timeout: nil)
736
+ if !url && !predicate
737
+ raise ArgumentError.new('url or predicate must be specified')
738
+ end
739
+ if predicate && !predicate.is_a?(Proc)
740
+ raise ArgumentError.new('predicate must be a proc.')
741
+ end
742
+ request_predicate =
743
+ if url
744
+ -> (request) { request.url == url }
745
+ else
746
+ -> (request) { predicate.call(request) }
747
+ end
748
+
749
+ wait_for_network_manager_event('Events.NetworkManager.Request',
750
+ predicate: request_predicate,
751
+ timeout: timeout,
752
+ )
753
+ end
754
+
755
+ define_async_method :async_wait_for_request
756
+
757
+ # @param url [String]
758
+ # @param predicate [Proc(Puppeteer::Request -> Boolean)]
759
+ private def wait_for_response(url: nil, predicate: nil, timeout: nil)
760
+ if !url && !predicate
761
+ raise ArgumentError.new('url or predicate must be specified')
762
+ end
763
+ if predicate && !predicate.is_a?(Proc)
764
+ raise ArgumentError.new('predicate must be a proc.')
765
+ end
766
+ response_predicate =
767
+ if url
768
+ -> (response) { response.url == url }
769
+ else
770
+ -> (response) { predicate.call(response) }
771
+ end
772
+
773
+ wait_for_network_manager_event('Events.NetworkManager.Response',
774
+ predicate: response_predicate,
775
+ timeout: timeout,
776
+ )
777
+ end
778
+
779
+ define_async_method :async_wait_for_response
727
780
 
728
781
  # @param timeout [number|nil]
729
782
  # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
@@ -762,20 +815,19 @@ class Puppeteer::Page
762
815
  @client.send_message('Emulation.setScriptExecutionDisabled', value: !enabled)
763
816
  end
764
817
 
765
- # /**
766
- # * @param {boolean} enabled
767
- # */
768
- # async setBypassCSP(enabled) {
769
- # await this._client.send('Page.setBypassCSP', { enabled });
770
- # }
818
+ # @param enabled [Boolean]
819
+ def bypass_csp=(enabled)
820
+ @client.send_message('Page.setBypassCSP', enabled: enabled)
821
+ end
771
822
 
772
- # /**
773
- # * @param {?string} type
774
- # */
775
- # async emulateMediaType(type) {
776
- # assert(type === 'screen' || type === 'print' || type === null, 'Unsupported media type: ' + type);
777
- # await this._client.send('Emulation.setEmulatedMedia', {media: type || ''});
778
- # }
823
+ # @param media_type [String|Symbol|nil] either of (media, print, nil)
824
+ def emulate_media_type(media_type)
825
+ media_type_str = media_type.to_s
826
+ unless ['screen', 'print', ''].include?(media_type_str)
827
+ raise ArgumentError.new("Unsupported media type: #{media_type}")
828
+ end
829
+ @client.send_message('Emulation.setEmulatedMedia', media: media_type_str)
830
+ end
779
831
 
780
832
  # /**
781
833
  # * @param {?Array<MediaFeature>} features
@@ -795,7 +847,7 @@ class Puppeteer::Page
795
847
 
796
848
  # @param timezone_id [String?]
797
849
  def emulate_timezone(timezone_id)
798
- @client.send_message('Emulation.setTimezoneOverride', timezoneId: timezoneId || '')
850
+ @client.send_message('Emulation.setTimezoneOverride', timezoneId: timezone_id || '')
799
851
  rescue => err
800
852
  if err.message.include?('Invalid timezone')
801
853
  raise ArgumentError.new("Invalid timezone ID: #{timezone_id}")
@@ -1034,16 +1086,6 @@ class Puppeteer::Page
1034
1086
 
1035
1087
  define_async_method :async_type_text
1036
1088
 
1037
- # /**
1038
- # * @param {(string|number|Function)} selectorOrFunctionOrTimeout
1039
- # * @param {!{visible?: boolean, hidden?: boolean, timeout?: number, polling?: string|number}=} options
1040
- # * @param {!Array<*>} args
1041
- # * @return {!Promise<!Puppeteer.JSHandle>}
1042
- # */
1043
- # waitFor(selectorOrFunctionOrTimeout, options = {}, ...args) {
1044
- # return this.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args);
1045
- # }
1046
-
1047
1089
  # @param selector [String]
1048
1090
  # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
1049
1091
  # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
@@ -0,0 +1,336 @@
1
+ class Puppeteer::Request
2
+ include Puppeteer::DebugPrint
3
+ include Puppeteer::IfPresent
4
+
5
+ # defines some methods used only in NetworkManager, Response
6
+ class InternalAccessor
7
+ def initialize(request)
8
+ @request = request
9
+ end
10
+
11
+ def request_id
12
+ @request.instance_variable_get(:@request_id)
13
+ end
14
+
15
+ def interception_id
16
+ @request.instance_variable_get(:@interception_id)
17
+ end
18
+
19
+ # @param response [Puppeteer::Response]
20
+ def response=(response)
21
+ @request.instance_variable_set(:@response, response)
22
+ end
23
+
24
+ def redirect_chain
25
+ @request.instance_variable_get(:@redirect_chain)
26
+ end
27
+
28
+ def failure_text=(failure_text)
29
+ @request.instance_variable_set(:@failure_text, failure_text)
30
+ end
31
+
32
+ def from_memory_cache=(from_memory_cache)
33
+ @request.instance_variable_set(:@from_memory_cache, from_memory_cache)
34
+ end
35
+
36
+ def from_memory_cache?
37
+ @request.instance_variable_get(:@from_memory_cache)
38
+ end
39
+ end
40
+
41
+ # @param client [Puppeteer::CDPSession]
42
+ # @param frame [Puppeteer::Frame]
43
+ # @param interception_id [string|nil]
44
+ # @param allow_interception [boolean]
45
+ # @param event [Hash]
46
+ # @param redirect_chain Array<Request>
47
+ def initialize(client, frame, interception_id, allow_interception, event, redirect_chain)
48
+ @client = client
49
+ @request_id = event['requestId']
50
+ @is_navigation_request = event['requestId'] == event['loaderId'] && event['type'] == 'Document'
51
+ @interception_id = interception_id
52
+ @allow_interception = allow_interception
53
+ @url = event['request']['url']
54
+ @resource_type = event['type'].downcase
55
+ @method = event['request']['method']
56
+ @post_data = event['request']['postData']
57
+ @frame = frame
58
+ @redirect_chain = redirect_chain
59
+ @headers = {}
60
+ event['request']['headers'].each do |key, value|
61
+ @headers[key.downcase] = value
62
+ end
63
+ @from_memory_cache = false
64
+
65
+ @internal = InternalAccessor.new(self)
66
+ end
67
+
68
+ attr_reader :internal
69
+ attr_reader :url, :resource_type, :method, :post_data, :headers, :response, :frame
70
+
71
+ def navigation_request?
72
+ @is_navigation_request
73
+ end
74
+
75
+ def redirect_chain
76
+ @redirect_chain.dup
77
+ end
78
+
79
+ def failure
80
+ if_present(@failure_text) do |failure_text|
81
+ { errorText: @failure_text }
82
+ end
83
+ end
84
+
85
+ private def headers_to_array(headers)
86
+ return nil unless headers
87
+
88
+ headers.map do |key, value|
89
+ { name: key, value: value.to_s }
90
+ end
91
+ end
92
+
93
+ class InterceptionNotEnabledError < StandardError
94
+ def initialize
95
+ super('Request Interception is not enabled!')
96
+ end
97
+ end
98
+
99
+ class AlreadyHandledError < StandardError
100
+ def initialize
101
+ super('Request is already handled!')
102
+ end
103
+ end
104
+
105
+ # proceed request on request interception.
106
+ #
107
+ # Example:
108
+ #
109
+ # ````
110
+ # page.on 'request' do |req|
111
+ # # Override headers
112
+ # headers = req.headers.merge(
113
+ # foo: 'bar', # set "foo" header
114
+ # origin: nil, # remove "origin" header
115
+ # )
116
+ # req.continue(headers: headers)
117
+ # end
118
+ # ```
119
+ #`
120
+ # @param error_code [String|Symbol]
121
+ def continue(url: nil, method: nil, post_data: nil, headers: nil)
122
+ # Request interception is not supported for data: urls.
123
+ return if @url.start_with?('data:')
124
+
125
+ unless @allow_interception
126
+ raise InterceptionNotEnabledError.new
127
+ end
128
+ if @interception_handled
129
+ raise AlreadyHandledError.new
130
+ end
131
+ @interception_handled = true
132
+
133
+ overrides = {
134
+ url: url,
135
+ method: method,
136
+ post_data: post_data,
137
+ headers: headers_to_array(headers),
138
+ }.compact
139
+ begin
140
+ @client.send_message('Fetch.continueRequest',
141
+ requestId: @interception_id,
142
+ **overrides,
143
+ )
144
+ rescue => err
145
+ # In certain cases, protocol will return error if the request was already canceled
146
+ # or the page was closed. We should tolerate these errors.
147
+ debug_puts(err)
148
+ end
149
+ end
150
+
151
+ # Mocking response.
152
+ #
153
+ # Example:
154
+ #
155
+ # ```
156
+ # page.on 'request' do |req|
157
+ # req.respond(
158
+ # status: 404,
159
+ # content_type: 'text/plain',
160
+ # body: 'Not Found!'
161
+ # )
162
+ # end
163
+ # ````
164
+ #
165
+ # @param status [Integer]
166
+ # @param headers [Hash<String, String>]
167
+ # @param content_type [String]
168
+ # @param body [String]
169
+ def respond(status: nil, headers: nil, content_type: nil, body: nil)
170
+ # Mocking responses for dataURL requests is not currently supported.
171
+ return if @url.start_with?('data:')
172
+
173
+ unless @allow_interception
174
+ raise InterceptionNotEnabledError.new
175
+ end
176
+ if @interception_handled
177
+ raise AlreadyHandledError.new
178
+ end
179
+ @interception_handled = true
180
+
181
+ mock_response_headers = {}
182
+ headers&.each do |key, value|
183
+ mock_response_headers[key.downcase] = value
184
+ end
185
+ if content_type
186
+ mock_response_headers['content-type'] = content_type
187
+ end
188
+ if body
189
+ mock_response_headers['content-length'] = body.length
190
+ end
191
+
192
+ mock_response = {
193
+ responseCode: status || 200,
194
+ responsePhrase: STATUS_TEXTS[(status || 200).to_s],
195
+ responseHeaders: headers_to_array(mock_response_headers),
196
+ body: if_present(body) { |mock_body| Base64.strict_encode64(mock_body) },
197
+ }.compact
198
+ begin
199
+ @client.send_message('Fetch.fulfillRequest',
200
+ requestId: @interception_id,
201
+ **mock_response,
202
+ )
203
+ rescue => err
204
+ # In certain cases, protocol will return error if the request was already canceled
205
+ # or the page was closed. We should tolerate these errors.
206
+ debug_puts(err)
207
+ end
208
+ end
209
+
210
+ # abort request on request interception.
211
+ #
212
+ # Example:
213
+ #
214
+ # ````
215
+ # page.on 'request' do |req|
216
+ # if req.url.include?("porn")
217
+ # req.abort
218
+ # else
219
+ # req.continue
220
+ # end
221
+ # end
222
+ # ```
223
+ #`
224
+ # @param error_code [String|Symbol]
225
+ def abort(error_code: :failed)
226
+ # Request interception is not supported for data: urls.
227
+ return if @url.start_with?('data:')
228
+
229
+ error_reason = ERROR_REASONS[error_code.to_s]
230
+ unless error_reason
231
+ raise ArgumentError.new("Unknown error code: #{error_code}")
232
+ end
233
+ unless @allow_interception
234
+ raise InterceptionNotEnabledError.new
235
+ end
236
+ if @interception_handled
237
+ raise AlreadyHandledError.new
238
+ end
239
+ @interception_handled = true
240
+
241
+ begin
242
+ @client.send_message('Fetch.failRequest',
243
+ requestId: @interception_id,
244
+ errorReason: error_reason,
245
+ )
246
+ rescue => err
247
+ # In certain cases, protocol will return error if the request was already canceled
248
+ # or the page was closed. We should tolerate these errors.
249
+ debug_puts(err)
250
+ end
251
+ end
252
+
253
+ ERROR_REASONS = {
254
+ 'aborted' => 'Aborted',
255
+ 'accessdenied' => 'AccessDenied',
256
+ 'addressunreachable' => 'AddressUnreachable',
257
+ 'blockedbyclient' => 'BlockedByClient',
258
+ 'blockedbyresponse' => 'BlockedByResponse',
259
+ 'connectionaborted' => 'ConnectionAborted',
260
+ 'connectionclosed' => 'ConnectionClosed',
261
+ 'connectionfailed' => 'ConnectionFailed',
262
+ 'connectionrefused' => 'ConnectionRefused',
263
+ 'connectionreset' => 'ConnectionReset',
264
+ 'internetdisconnected' => 'InternetDisconnected',
265
+ 'namenotresolved' => 'NameNotResolved',
266
+ 'timedout' => 'TimedOut',
267
+ 'failed' => 'Failed',
268
+ }.freeze
269
+
270
+ # List taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml with extra 306 and 418 codes.
271
+ STATUS_TEXTS = {
272
+ '100' => 'Continue',
273
+ '101' => 'Switching Protocols',
274
+ '102' => 'Processing',
275
+ '103' => 'Early Hints',
276
+ '200' => 'OK',
277
+ '201' => 'Created',
278
+ '202' => 'Accepted',
279
+ '203' => 'Non-Authoritative Information',
280
+ '204' => 'No Content',
281
+ '205' => 'Reset Content',
282
+ '206' => 'Partial Content',
283
+ '207' => 'Multi-Status',
284
+ '208' => 'Already Reported',
285
+ '226' => 'IM Used',
286
+ '300' => 'Multiple Choices',
287
+ '301' => 'Moved Permanently',
288
+ '302' => 'Found',
289
+ '303' => 'See Other',
290
+ '304' => 'Not Modified',
291
+ '305' => 'Use Proxy',
292
+ '306' => 'Switch Proxy',
293
+ '307' => 'Temporary Redirect',
294
+ '308' => 'Permanent Redirect',
295
+ '400' => 'Bad Request',
296
+ '401' => 'Unauthorized',
297
+ '402' => 'Payment Required',
298
+ '403' => 'Forbidden',
299
+ '404' => 'Not Found',
300
+ '405' => 'Method Not Allowed',
301
+ '406' => 'Not Acceptable',
302
+ '407' => 'Proxy Authentication Required',
303
+ '408' => 'Request Timeout',
304
+ '409' => 'Conflict',
305
+ '410' => 'Gone',
306
+ '411' => 'Length Required',
307
+ '412' => 'Precondition Failed',
308
+ '413' => 'Payload Too Large',
309
+ '414' => 'URI Too Long',
310
+ '415' => 'Unsupported Media Type',
311
+ '416' => 'Range Not Satisfiable',
312
+ '417' => 'Expectation Failed',
313
+ '418' => 'I\'m a teapot',
314
+ '421' => 'Misdirected Request',
315
+ '422' => 'Unprocessable Entity',
316
+ '423' => 'Locked',
317
+ '424' => 'Failed Dependency',
318
+ '425' => 'Too Early',
319
+ '426' => 'Upgrade Required',
320
+ '428' => 'Precondition Required',
321
+ '429' => 'Too Many Requests',
322
+ '431' => 'Request Header Fields Too Large',
323
+ '451' => 'Unavailable For Legal Reasons',
324
+ '500' => 'Internal Server Error',
325
+ '501' => 'Not Implemented',
326
+ '502' => 'Bad Gateway',
327
+ '503' => 'Service Unavailable',
328
+ '504' => 'Gateway Timeout',
329
+ '505' => 'HTTP Version Not Supported',
330
+ '506' => 'Variant Also Negotiates',
331
+ '507' => 'Insufficient Storage',
332
+ '508' => 'Loop Detected',
333
+ '510' => 'Not Extended',
334
+ '511' => 'Network Authentication Required',
335
+ }.freeze
336
+ end
@@ -0,0 +1,113 @@
1
+ require 'json'
2
+
3
+ class Puppeteer::Response
4
+ include Puppeteer::IfPresent
5
+
6
+ class Redirected < StandardError
7
+ def initialize
8
+ super('Response body is unavailable for redirect responses')
9
+ end
10
+ end
11
+
12
+ # defines methods used only in NetworkManager
13
+ class InternalAccessor
14
+ def initialize(response)
15
+ @response = response
16
+ end
17
+
18
+ def body_loaded_promise
19
+ @response.instance_variable_get(:@body_loaded_promise)
20
+ end
21
+ end
22
+
23
+ class RemoteAddress
24
+ def initialize(ip:, port:)
25
+ @ip = ip
26
+ @port = port
27
+ end
28
+ attr_reader :ip, :port
29
+ end
30
+
31
+ # @param client [Puppeteer::CDPSession]
32
+ # @param request [Puppeteer::Request]
33
+ # @param response_payload [Hash]
34
+ def initialize(client, request, response_payload)
35
+ @client = client
36
+ @request = request
37
+
38
+ @body_loaded_promise = resolvable_future
39
+ @remote_address = RemoteAddress.new(
40
+ ip: response_payload['remoteIPAddress'],
41
+ port: response_payload['remotePort'],
42
+ )
43
+
44
+ @status = response_payload['status']
45
+ @status_text = response_payload['statusText']
46
+ @url = request.url
47
+ @from_disk_cache = !!response_payload['fromDiskCache']
48
+ @from_service_worker = !!response_payload['fromServiceWorker']
49
+
50
+ @headers = {}
51
+ response_payload['headers'].each do |key, value|
52
+ @headers[key.downcase] = value
53
+ end
54
+ @security_details = if_present(response_payload['securityDetails']) do |security_payload|
55
+ SecurityDetails.new(security_payload)
56
+ end
57
+
58
+ @internal = InternalAccessor.new(self)
59
+ end
60
+
61
+ attr_reader :internal
62
+
63
+ attr_reader :remote_address, :url, :status, :status_text, :headers, :security_details, :request
64
+
65
+ # @return [Boolean]
66
+ def ok?
67
+ @status == 0 || (@status >= 200 && @status <= 299)
68
+ end
69
+
70
+ def buffer
71
+ await @body_loaded_promise
72
+ response = @client.send_message('Network.getResponseBody', requestId: @request.internal.request_id)
73
+ if response['base64Encoded']
74
+ Base64.decode64(response['body'])
75
+ else
76
+ response['body']
77
+ end
78
+ end
79
+
80
+ # @param text [String]
81
+ def text
82
+ buffer
83
+ end
84
+
85
+ # @param json [Hash]
86
+ def json
87
+ JSON.parse(text)
88
+ end
89
+
90
+ def from_cache?
91
+ @from_disk_cache || @request.internal.from_memory_cache?
92
+ end
93
+
94
+ def from_service_worker?
95
+ @from_service_worker
96
+ end
97
+
98
+ def frame
99
+ @request.frame
100
+ end
101
+
102
+ class SecurityDetails
103
+ def initialize(security_payload)
104
+ @subject_name = security_payload['subjectName']
105
+ @issuer = security_payload['issuer']
106
+ @valid_from = security_payload['validFrom']
107
+ @valid_to = security_payload['validTo']
108
+ @protocol = security_payload['protocol']
109
+ end
110
+
111
+ attr_reader :subject_name, :issuer, :valid_from, :valid_to, :protocol
112
+ end
113
+ end
@@ -1,3 +1,3 @@
1
1
  class Puppeteer
2
- VERSION = '0.0.15'
2
+ VERSION = '0.0.16'
3
3
  end
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.0.15
4
+ version: 0.0.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - YusukeIwaki
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-06-28 00:00:00.000000000 Z
11
+ date: 2020-07-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -227,6 +227,8 @@ files:
227
227
  - lib/puppeteer/page/pdf_options.rb
228
228
  - lib/puppeteer/page/screenshot_options.rb
229
229
  - lib/puppeteer/remote_object.rb
230
+ - lib/puppeteer/request.rb
231
+ - lib/puppeteer/response.rb
230
232
  - lib/puppeteer/target.rb
231
233
  - lib/puppeteer/timeout_settings.rb
232
234
  - lib/puppeteer/touch_screen.rb