puppeteer-ruby 0.0.15 → 0.0.20

Sign up to get free protection for your applications and to get access to all the features.
@@ -94,5 +94,21 @@ class Puppeteer::Mouse
94
94
  )
95
95
  end
96
96
 
97
+ # Dispatches a `mousewheel` event.
98
+ #
99
+ # @param delta_x [Integer]
100
+ # @param delta_y [Integer]
101
+ def wheel(delta_x: 0, delta_y: 0)
102
+ @client.send_message('Input.dispatchMouseEvent',
103
+ type: 'mouseWheel',
104
+ x: @x,
105
+ y: @y,
106
+ deltaX: delta_x,
107
+ deltaY: delta_y,
108
+ modifiers: @keyboard.modifiers,
109
+ pointerType: 'mouse',
110
+ )
111
+ end
112
+
97
113
  define_async_method :async_up
98
114
  end
@@ -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
@@ -101,7 +101,9 @@ class Puppeteer::Page
101
101
  end
102
102
  # client.on('Runtime.consoleAPICalled', event => this._onConsoleAPI(event));
103
103
  # client.on('Runtime.bindingCalled', event => this._onBindingCalled(event));
104
- # client.on('Page.javascriptDialogOpening', event => this._onDialog(event));
104
+ @client.on_event 'Page.javascriptDialogOpening' do |event|
105
+ handle_dialog_opening(event)
106
+ end
105
107
  # client.on('Runtime.exceptionThrown', exception => this._handleException(exception.exceptionDetails));
106
108
  # client.on('Inspector.targetCrashed', event => this._onTargetCrashed());
107
109
  # client.on('Performance.metrics', event => this._emitMetrics(event));
@@ -129,7 +131,7 @@ class Puppeteer::Page
129
131
  EVENT_MAPPINGS = {
130
132
  close: 'Events.Page.Close',
131
133
  # console: 'Events.Page.Console',
132
- # dialog: 'Events.Page.Dialog',
134
+ dialog: 'Events.Page.Dialog',
133
135
  domcontentloaded: 'Events.Page.DOMContentLoaded',
134
136
  # error:
135
137
  frameattached: 'Events.Page.FrameAttached',
@@ -266,7 +268,13 @@ class Puppeteer::Page
266
268
  @frame_manager.main_frame
267
269
  end
268
270
 
269
- attr_reader :keyboard, :touch_screen, :coverage, :accessibility
271
+ attr_reader :touch_screen, :coverage, :accessibility
272
+
273
+ def keyboard(&block)
274
+ @keyboard.instance_eval(&block) unless block.nil?
275
+
276
+ @keyboard
277
+ end
270
278
 
271
279
  def frames
272
280
  @frame_manager.frames
@@ -626,20 +634,17 @@ class Puppeteer::Page
626
634
  # this.emit(Events.Page.Console, message);
627
635
  # }
628
636
 
629
- # _onDialog(event) {
630
- # let dialogType = null;
631
- # if (event.type === 'alert')
632
- # dialogType = Dialog.Type.Alert;
633
- # else if (event.type === 'confirm')
634
- # dialogType = Dialog.Type.Confirm;
635
- # else if (event.type === 'prompt')
636
- # dialogType = Dialog.Type.Prompt;
637
- # else if (event.type === 'beforeunload')
638
- # dialogType = Dialog.Type.BeforeUnload;
639
- # assert(dialogType, 'Unknown javascript dialog type: ' + event.type);
640
- # const dialog = new Dialog(this._client, dialogType, event.message, event.defaultPrompt);
641
- # this.emit(Events.Page.Dialog, dialog);
642
- # }
637
+ private def handle_dialog_opening(event)
638
+ dialog_type = event['type']
639
+ unless %w(alert confirm prompt beforeunload).include?(dialog_type)
640
+ raise ArgumentError.new("Unknown javascript dialog type: #{dialog_type}")
641
+ end
642
+ dialog = Puppeteer::Dialog.new(@client,
643
+ type: dialog_type,
644
+ message: event['message'],
645
+ default_value: event['defaultPrompt'])
646
+ emit_event('Events.Page.Dialog', dialog)
647
+ end
643
648
 
644
649
  # @return [String]
645
650
  def url
@@ -681,49 +686,111 @@ class Puppeteer::Page
681
686
  ).first
682
687
  end
683
688
 
684
- # @param timeout [number|nil]
685
- # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
686
689
  private def wait_for_navigation(timeout: nil, wait_until: nil)
687
690
  main_frame.send(:wait_for_navigation, timeout: timeout, wait_until: wait_until)
688
691
  end
689
692
 
693
+ # @!method async_wait_for_navigation(timeout: nil, wait_until: nil)
694
+ #
695
+ # @param timeout [number|nil]
696
+ # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
690
697
  define_async_method :async_wait_for_navigation
691
698
 
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
- # }
699
+ private def wait_for_network_manager_event(event_name, predicate:, timeout:)
700
+ option_timeout = timeout || @timeout_settings.timeout
709
701
 
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
- # }
702
+ @wait_for_network_manager_event_listener_ids ||= {}
703
+ if_present(@wait_for_network_manager_event_listener_ids[event_name]) do |listener_id|
704
+ @frame_manager.network_manager.remove_event_listener(listener_id)
705
+ end
706
+
707
+ promise = resolvable_future
708
+
709
+ @wait_for_network_manager_event_listener_ids[event_name] =
710
+ @frame_manager.network_manager.add_event_listener(event_name) do |event_target|
711
+ if predicate.call(event_target)
712
+ promise.fulfill(nil)
713
+ end
714
+ end
715
+
716
+ begin
717
+ Timeout.timeout(option_timeout / 1000.0) do
718
+ await_any(promise, session_close_promise)
719
+ end
720
+ rescue Timeout::Error
721
+ raise Puppeteer::TimeoutError.new("waiting for #{event_name} failed: timeout #{timeout}ms exceeded")
722
+ ensure
723
+ @frame_manager.network_manager.remove_event_listener(@wait_for_network_manager_event_listener_ids[event_name])
724
+ end
725
+ end
726
+
727
+ private def session_close_promise
728
+ @disconnect_promise ||= resolvable_future do |future|
729
+ @client.observe_first('Events.CDPSession.Disconnected') do
730
+ future.reject(Puppeteer::CDPSession::Error.new('Target Closed'))
731
+ end
732
+ end
733
+ end
734
+
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
+ # @!method async_wait_for_request(url: nil, predicate: nil, timeout: nil)
756
+ #
757
+ # Waits until request URL matches or request matches the given predicate.
758
+ #
759
+ # Waits until request URL matches
760
+ # wait_for_request(url: 'https://example.com/awesome')
761
+ #
762
+ # Waits until request matches the given predicate
763
+ # wait_for_request(predicate: -> (req){ req.url.start_with?('https://example.com/search') })
764
+ #
765
+ # @param url [String]
766
+ # @param predicate [Proc(Puppeteer::Request -> Boolean)]
767
+ define_async_method :async_wait_for_request
768
+
769
+ private def wait_for_response(url: nil, predicate: nil, timeout: nil)
770
+ if !url && !predicate
771
+ raise ArgumentError.new('url or predicate must be specified')
772
+ end
773
+ if predicate && !predicate.is_a?(Proc)
774
+ raise ArgumentError.new('predicate must be a proc.')
775
+ end
776
+ response_predicate =
777
+ if url
778
+ -> (response) { response.url == url }
779
+ else
780
+ -> (response) { predicate.call(response) }
781
+ end
782
+
783
+ wait_for_network_manager_event('Events.NetworkManager.Response',
784
+ predicate: response_predicate,
785
+ timeout: timeout,
786
+ )
787
+ end
788
+
789
+ # @!method async_wait_for_response(url: nil, predicate: nil, timeout: nil)
790
+ #
791
+ # @param url [String]
792
+ # @param predicate [Proc(Puppeteer::Request -> Boolean)]
793
+ define_async_method :async_wait_for_response
727
794
 
728
795
  # @param timeout [number|nil]
729
796
  # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
@@ -762,20 +829,19 @@ class Puppeteer::Page
762
829
  @client.send_message('Emulation.setScriptExecutionDisabled', value: !enabled)
763
830
  end
764
831
 
765
- # /**
766
- # * @param {boolean} enabled
767
- # */
768
- # async setBypassCSP(enabled) {
769
- # await this._client.send('Page.setBypassCSP', { enabled });
770
- # }
832
+ # @param enabled [Boolean]
833
+ def bypass_csp=(enabled)
834
+ @client.send_message('Page.setBypassCSP', enabled: enabled)
835
+ end
771
836
 
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
- # }
837
+ # @param media_type [String|Symbol|nil] either of (media, print, nil)
838
+ def emulate_media_type(media_type)
839
+ media_type_str = media_type.to_s
840
+ unless ['screen', 'print', ''].include?(media_type_str)
841
+ raise ArgumentError.new("Unsupported media type: #{media_type}")
842
+ end
843
+ @client.send_message('Emulation.setEmulatedMedia', media: media_type_str)
844
+ end
779
845
 
780
846
  # /**
781
847
  # * @param {?Array<MediaFeature>} features
@@ -795,7 +861,7 @@ class Puppeteer::Page
795
861
 
796
862
  # @param timezone_id [String?]
797
863
  def emulate_timezone(timezone_id)
798
- @client.send_message('Emulation.setTimezoneOverride', timezoneId: timezoneId || '')
864
+ @client.send_message('Emulation.setTimezoneOverride', timezoneId: timezone_id || '')
799
865
  rescue => err
800
866
  if err.message.include?('Invalid timezone')
801
867
  raise ArgumentError.new("Invalid timezone ID: #{timezone_id}")
@@ -1019,8 +1085,19 @@ class Puppeteer::Page
1019
1085
  define_async_method :async_select
1020
1086
 
1021
1087
  # @param selector [String]
1022
- def tap(selector)
1023
- main_frame.tap(selector)
1088
+ def tap(selector: nil, &block)
1089
+ # resolves double meaning of tap.
1090
+ if selector.nil? && block
1091
+ # Original usage of Object#tap.
1092
+ #
1093
+ # browser.new_page.tap do |page|
1094
+ # ...
1095
+ # end
1096
+ super(&block)
1097
+ else
1098
+ # Puppeteer's Page#tap.
1099
+ main_frame.tap(selector)
1100
+ end
1024
1101
  end
1025
1102
 
1026
1103
  define_async_method :async_tap
@@ -1034,16 +1111,6 @@ class Puppeteer::Page
1034
1111
 
1035
1112
  define_async_method :async_type_text
1036
1113
 
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
1114
  # @param selector [String]
1048
1115
  # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
1049
1116
  # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
@@ -1054,6 +1121,11 @@ class Puppeteer::Page
1054
1121
 
1055
1122
  define_async_method :async_wait_for_selector
1056
1123
 
1124
+ # @param milliseconds [Integer] the number of milliseconds to wait.
1125
+ def wait_for_timeout(milliseconds)
1126
+ main_frame.wait_for_timeout(milliseconds)
1127
+ end
1128
+
1057
1129
  # @param xpath [String]
1058
1130
  # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
1059
1131
  # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
@@ -1064,12 +1136,13 @@ class Puppeteer::Page
1064
1136
 
1065
1137
  define_async_method :async_wait_for_xpath
1066
1138
 
1067
- # @param {Function|string} pageFunction
1068
- # @param {!{polling?: string|number, timeout?: number}=} options
1069
- # @param {!Array<*>} args
1070
- # @return {!Promise<!Puppeteer.JSHandle>}
1071
- def wait_for_function(page_function, options = {}, *args)
1072
- main_frame.wait_for_function(page_function, options, *args)
1139
+ # @param page_function [String]
1140
+ # @param args [Integer|Array]
1141
+ # @param polling [String]
1142
+ # @param timeout [Integer]
1143
+ # @return [Puppeteer::JSHandle]
1144
+ def wait_for_function(page_function, args: [], polling: nil, timeout: nil)
1145
+ main_frame.wait_for_function(page_function, args: args, polling: polling, timeout: timeout)
1073
1146
  end
1074
1147
 
1075
1148
  define_async_method :async_wait_for_function