puppeteer-ruby 0.0.15 → 0.0.20

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.
@@ -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