puppeteer-ruby 0.37.1 → 0.38.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,4 @@
1
- class Puppeteer::Request
1
+ class Puppeteer::HTTPRequest
2
2
  include Puppeteer::DebugPrint
3
3
  include Puppeteer::IfPresent
4
4
 
@@ -16,7 +16,7 @@ class Puppeteer::Request
16
16
  @request.instance_variable_get(:@interception_id)
17
17
  end
18
18
 
19
- # @param response [Puppeteer::Response]
19
+ # @param response [Puppeteer::HTTPResponse]
20
20
  def response=(response)
21
21
  @request.instance_variable_set(:@response, response)
22
22
  end
@@ -56,6 +56,12 @@ class Puppeteer::Request
56
56
  @post_data = event['request']['postData']
57
57
  @frame = frame
58
58
  @redirect_chain = redirect_chain
59
+ @continue_request_overrides = {}
60
+ @current_strategy = 'none'
61
+ @current_priority = nil
62
+ @intercept_actions = []
63
+ @initiator = event['initiator']
64
+
59
65
  @headers = {}
60
66
  event['request']['headers'].each do |key, value|
61
67
  @headers[key.downcase] = value
@@ -66,7 +72,85 @@ class Puppeteer::Request
66
72
  end
67
73
 
68
74
  attr_reader :internal
69
- attr_reader :url, :resource_type, :method, :post_data, :headers, :response, :frame
75
+ attr_reader :url, :resource_type, :method, :post_data, :headers, :response, :frame, :initiator
76
+
77
+ def inspect
78
+ values = %i[request_id method url].map do |sym|
79
+ value = instance_variable_get(:"@#{sym}")
80
+ "@#{sym}=#{value}"
81
+ end
82
+ "#<Puppeteer::HTTPRequest #{values.join(' ')}>"
83
+ end
84
+
85
+ private def assert_interception_allowed
86
+ unless @allow_interception
87
+ raise InterceptionNotEnabledError.new
88
+ end
89
+ end
90
+
91
+ private def assert_interception_not_handled
92
+ if @interception_handled
93
+ raise AlreadyHandledError.new
94
+ end
95
+ end
96
+
97
+ # @returns the `ContinueRequestOverrides` that will be used
98
+ # if the interception is allowed to continue (ie, `abort()` and
99
+ # `respond()` aren't called).
100
+ def continue_request_overrides
101
+ assert_interception_allowed
102
+ @continue_request_overrides
103
+ end
104
+
105
+ # @returns The `ResponseForRequest` that gets used if the
106
+ # interception is allowed to respond (ie, `abort()` is not called).
107
+ def response_for_request
108
+ assert_interception_allowed
109
+ @response_for_request
110
+ end
111
+
112
+ # @returns the most recent reason for aborting the request
113
+ def abort_error_reason
114
+ assert_interception_allowed
115
+ @abort_error_reason
116
+ end
117
+
118
+ # @returns An array of the current intercept resolution strategy and priority
119
+ # `[strategy,priority]`. Strategy is one of: `abort`, `respond`, `continue`,
120
+ # `disabled`, `none`, or `already-handled`.
121
+ def intercept_resolution
122
+ if !@allow_interception
123
+ ['disabled']
124
+ elsif @interception_handled
125
+ ['already-handled']
126
+ else
127
+ [@current_strategy, @current_priority]
128
+ end
129
+ end
130
+
131
+ # Adds an async request handler to the processing queue.
132
+ # Deferred handlers are not guaranteed to execute in any particular order,
133
+ # but they are guarnateed to resolve before the request interception
134
+ # is finalized.
135
+ #
136
+ # @param pending_handler [Proc]
137
+ def enqueue_intercept_action(pending_handler)
138
+ @intercept_actions << pending_handler
139
+ end
140
+
141
+ # Awaits pending interception handlers and then decides how to fulfill
142
+ # the request interception.
143
+ def finalize_interceptions
144
+ @intercept_actions.each(&:call)
145
+ case @intercept_resolution
146
+ when :abort
147
+ abort_impl(**@abort_error_reason)
148
+ when :respond
149
+ respond_impl(**@response_for_request)
150
+ when :continue
151
+ continue_impl(@continue_request_overrides)
152
+ end
153
+ end
70
154
 
71
155
  def navigation_request?
72
156
  @is_navigation_request
@@ -116,24 +200,43 @@ class Puppeteer::Request
116
200
  # end
117
201
  #
118
202
  # @param error_code [String|Symbol]
119
- def continue(url: nil, method: nil, post_data: nil, headers: nil)
203
+ def continue(url: nil, method: nil, post_data: nil, headers: nil, priority: nil)
120
204
  # Request interception is not supported for data: urls.
121
205
  return if @url.start_with?('data:')
122
206
 
123
- unless @allow_interception
124
- raise InterceptionNotEnabledError.new
125
- end
126
- if @interception_handled
127
- raise AlreadyHandledError.new
128
- end
129
- @interception_handled = true
207
+ assert_interception_allowed
208
+ assert_interception_not_handled
130
209
 
131
210
  overrides = {
132
211
  url: url,
133
212
  method: method,
134
- post_data: post_data,
213
+ postData: post_data,
135
214
  headers: headers_to_array(headers),
136
215
  }.compact
216
+
217
+ if priority.nil?
218
+ continue_impl(overrides)
219
+ return
220
+ end
221
+
222
+ @continue_request_overrides = overrides
223
+ if @current_priority.nil? || priority > @current_priority
224
+ @current_strategy = :continue
225
+ @current_priority = priority
226
+ return
227
+ end
228
+
229
+ if priority == @current_priority
230
+ if @current_strategy == :abort || @current_strategy == :respond
231
+ return
232
+ end
233
+ @current_strategy = :continue
234
+ end
235
+ end
236
+
237
+ private def continue_impl(overrides)
238
+ @interception_handled = true
239
+
137
240
  begin
138
241
  @client.send_message('Fetch.continueRequest',
139
242
  requestId: @interception_id,
@@ -162,16 +265,40 @@ class Puppeteer::Request
162
265
  # @param headers [Hash<String, String>]
163
266
  # @param content_type [String]
164
267
  # @param body [String]
165
- def respond(status: nil, headers: nil, content_type: nil, body: nil)
268
+ def respond(status: nil, headers: nil, content_type: nil, body: nil, priority: nil)
166
269
  # Mocking responses for dataURL requests is not currently supported.
167
270
  return if @url.start_with?('data:')
168
271
 
169
- unless @allow_interception
170
- raise InterceptionNotEnabledError.new
272
+ assert_interception_allowed
273
+ assert_interception_not_handled
274
+
275
+ if priority.nil?
276
+ respond_impl(status: status, headers: headers, content_type: content_type, body: body)
277
+ return
171
278
  end
172
- if @interception_handled
173
- raise AlreadyHandledError.new
279
+
280
+ @response_for_request = {
281
+ status: status,
282
+ headers: headers,
283
+ content_type: content_type,
284
+ body: body,
285
+ }
286
+
287
+ if @current_priority.nil? || priority > @current_priority
288
+ @current_strategy = :respond
289
+ @current_priority = priority
290
+ return
291
+ end
292
+
293
+ if priority == @current_priority
294
+ if @current_strategy == :abort
295
+ return
296
+ end
297
+ @current_strategy = :respond
174
298
  end
299
+ end
300
+
301
+ private def respond_impl(status: nil, headers: nil, content_type: nil, body: nil)
175
302
  @interception_handled = true
176
303
 
177
304
  mock_response_headers = {}
@@ -191,6 +318,7 @@ class Puppeteer::Request
191
318
  responseHeaders: headers_to_array(mock_response_headers),
192
319
  body: if_present(body) { |mock_body| Base64.strict_encode64(mock_body) },
193
320
  }.compact
321
+
194
322
  begin
195
323
  @client.send_message('Fetch.fulfillRequest',
196
324
  requestId: @interception_id,
@@ -216,7 +344,7 @@ class Puppeteer::Request
216
344
  # end
217
345
  #
218
346
  # @param error_code [String|Symbol]
219
- def abort(error_code: :failed)
347
+ def abort(error_code: :failed, priority: nil)
220
348
  # Request interception is not supported for data: urls.
221
349
  return if @url.start_with?('data:')
222
350
 
@@ -224,12 +352,21 @@ class Puppeteer::Request
224
352
  unless error_reason
225
353
  raise ArgumentError.new("Unknown error code: #{error_code}")
226
354
  end
227
- unless @allow_interception
228
- raise InterceptionNotEnabledError.new
355
+ assert_interception_allowed
356
+ assert_interception_not_handled
357
+
358
+ if priority.nil?
359
+ abort_impl(error_reason)
229
360
  end
230
- if @interception_handled
231
- raise AlreadyHandledError.new
361
+ @abort_error_reason = error_reason
362
+
363
+ if @current_priority.nil? || priority > @current_priority
364
+ @current_strategy = :abort
365
+ @current_priority = priority
232
366
  end
367
+ end
368
+
369
+ private def abort_impl(error_reason)
233
370
  @interception_handled = true
234
371
 
235
372
  begin
@@ -1,6 +1,6 @@
1
1
  require 'json'
2
2
 
3
- class Puppeteer::Response
3
+ class Puppeteer::HTTPResponse
4
4
  include Puppeteer::IfPresent
5
5
 
6
6
  class Redirected < StandardError
@@ -29,7 +29,7 @@ class Puppeteer::Response
29
29
  end
30
30
 
31
31
  # @param client [Puppeteer::CDPSession]
32
- # @param request [Puppeteer::Request]
32
+ # @param request [Puppeteer::HTTPRequest]
33
33
  # @param response_payload [Hash]
34
34
  def initialize(client, request, response_payload)
35
35
  @client = client
@@ -34,13 +34,13 @@ module Puppeteer::Launcher
34
34
  if @launch_options.pipe?
35
35
  chrome_arguments << '--remote-debugging-pipe'
36
36
  else
37
- chrome_arguments << '--remote-debugging-port=0'
37
+ chrome_arguments << "--remote-debugging-port=#{@chrome_arg_options.debugging_port}"
38
38
  end
39
39
  end
40
40
 
41
41
  temporary_user_data_dir = nil
42
42
  if chrome_arguments.none? { |arg| arg.start_with?('--user-data-dir') }
43
- temporary_user_data_dir = Dir.mktmpdir('puppeteer_dev_chrome_profile-')
43
+ temporary_user_data_dir = Dir.mktmpdir('puppeteer_dev_chrome_profile-', ENV['PUPPETEER_TMP_DIR'])
44
44
  chrome_arguments << "--user-data-dir=#{temporary_user_data_dir}"
45
45
  end
46
46
 
@@ -48,7 +48,7 @@ module Puppeteer::Launcher
48
48
  if @launch_options.channel
49
49
  executable_path_for_channel(@launch_options.channel.to_s)
50
50
  else
51
- @launch_options.executable_path || executable_path_for_channel('chrome')
51
+ @launch_options.executable_path || fallback_executable_path
52
52
  end
53
53
  use_pipe = chrome_arguments.include?('--remote-debugging-pipe')
54
54
  runner = Puppeteer::BrowserRunner.new(chrome_executable, chrome_arguments, temporary_user_data_dir)
@@ -78,7 +78,10 @@ module Puppeteer::Launcher
78
78
  close_callback: -> { runner.close },
79
79
  )
80
80
 
81
- browser.wait_for_target(predicate: ->(target) { target.type == 'page' })
81
+ browser.wait_for_target(
82
+ predicate: ->(target) { target.type == 'page' },
83
+ timeout: @launch_options.timeout,
84
+ )
82
85
 
83
86
  browser
84
87
  rescue
@@ -216,10 +219,14 @@ module Puppeteer::Launcher
216
219
  if channel
217
220
  executable_path_for_channel(channel.to_s)
218
221
  else
219
- executable_path_for_channel('chrome')
222
+ fallback_executable_path
220
223
  end
221
224
  end
222
225
 
226
+ private def fallback_executable_path
227
+ executable_path_for_channel('chrome')
228
+ end
229
+
223
230
  CHROMIUM_CHANNELS = {
224
231
  windows: {
225
232
  'chrome' => "#{ENV['PROGRAMFILES']}\\Google\\Chrome\\Application\\chrome.exe",
@@ -236,7 +243,16 @@ module Puppeteer::Launcher
236
243
  'msedge' => '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
237
244
  },
238
245
  linux: {
239
- 'chrome' => '/opt/google/chrome/chrome',
246
+ 'chrome' => -> {
247
+ Puppeteer::ExecutablePathFinder.new(
248
+ 'google-chrome-stable',
249
+ 'google-chrome',
250
+ 'chrome',
251
+ 'chromium-freeworld',
252
+ 'chromium-browser',
253
+ 'chromium',
254
+ ).find_first
255
+ },
240
256
  'chrome-beta' => '/opt/google/chrome-beta/chrome',
241
257
  'chrome-dev' => '/opt/google/chrome-unstable/chrome',
242
258
  },
@@ -254,6 +270,10 @@ module Puppeteer::Launcher
254
270
  end
255
271
 
256
272
  chrome_path = chrome_path_map[channel]
273
+ if chrome_path.is_a?(Proc)
274
+ chrome_path = chrome_path.call
275
+ end
276
+
257
277
  unless chrome_path
258
278
  raise ArgumentError.new("Invalid channel: '#{channel}'. Allowed channel is #{chrome_path_map.keys}")
259
279
  end
@@ -34,9 +34,10 @@ module Puppeteer::Launcher
34
34
  if @headless.nil?
35
35
  @headless = !@devtools
36
36
  end
37
+ @debugging_port = options[:debugging_port] || 0
37
38
  end
38
39
 
39
- attr_reader :args, :user_data_dir
40
+ attr_reader :args, :user_data_dir, :debugging_port
40
41
 
41
42
  def headless?
42
43
  @headless
@@ -28,7 +28,7 @@ module Puppeteer::Launcher
28
28
  end
29
29
 
30
30
  if firefox_arguments.none? { |arg| arg.start_with?('--remote-debugging-') }
31
- firefox_arguments << '--remote-debugging-port=0'
31
+ firefox_arguments << "--remote-debugging-port=#{@chrome_arg_options.debugging_port}"
32
32
  end
33
33
 
34
34
  temporary_user_data_dir = nil
@@ -42,7 +42,7 @@ module Puppeteer::Launcher
42
42
  if @launch_options.channel
43
43
  executable_path_for_channel(@launch_options.channel.to_s)
44
44
  else
45
- @launch_options.executable_path || executable_path_for_channel('nightly')
45
+ @launch_options.executable_path || fallback_executable_path
46
46
  end
47
47
  runner = Puppeteer::BrowserRunner.new(firefox_executable, firefox_arguments, temporary_user_data_dir)
48
48
  runner.start(
@@ -71,7 +71,10 @@ module Puppeteer::Launcher
71
71
  close_callback: -> { runner.close },
72
72
  )
73
73
 
74
- browser.wait_for_target(predicate: ->(target) { target.type == 'page' })
74
+ browser.wait_for_target(
75
+ predicate: ->(target) { target.type == 'page' },
76
+ timeout: @launch_options.timeout,
77
+ )
75
78
 
76
79
  browser
77
80
  rescue
@@ -138,14 +141,18 @@ module Puppeteer::Launcher
138
141
  if channel
139
142
  executable_path_for_channel(channel.to_s)
140
143
  else
141
- executable_path_for_channel('firefox')
144
+ fallback_executable_path
142
145
  end
143
146
  end
144
147
 
148
+ private def fallback_executable_path
149
+ executable_path_for_channel('firefox')
150
+ end
151
+
145
152
  FIREFOX_EXECUTABLE_PATHS = {
146
153
  windows: "#{ENV['PROGRAMFILES']}\\Firefox Nightly\\firefox.exe",
147
154
  darwin: '/Applications/Firefox Nightly.app/Contents/MacOS/firefox',
148
- linux: '/usr/bin/firefox',
155
+ linux: -> { Puppeteer::ExecutablePathFinder.new('firefox').find_first },
149
156
  }.freeze
150
157
 
151
158
  # @param channel [String]
@@ -163,6 +170,9 @@ module Puppeteer::Launcher
163
170
  else
164
171
  FIREFOX_EXECUTABLE_PATHS[:linux]
165
172
  end
173
+ if firefox_path.is_a?(Proc)
174
+ firefox_path = firefox_path.call
175
+ end
166
176
 
167
177
  unless File.exist?(firefox_path)
168
178
  raise "Nightly version of Firefox is not installed on this system.\nExpected path: #{firefox_path}"
@@ -221,7 +231,7 @@ module Puppeteer::Launcher
221
231
  end
222
232
 
223
233
  private def create_profile(extra_prefs = {})
224
- Dir.mktmpdir('puppeteer_dev_firefox_profile-').tap do |profile_path|
234
+ Dir.mktmpdir('puppeteer_dev_firefox_profile-', ENV['PUPPETEER_TMP_DIR']).tap do |profile_path|
225
235
  server = 'dummy.test'
226
236
  default_preferences = {
227
237
  # Make sure Shield doesn't hit the network.
@@ -88,7 +88,7 @@ class Puppeteer::LifecycleWatcher
88
88
  check_lifecycle_complete
89
89
  end
90
90
 
91
- # @param [Puppeteer::Request] request
91
+ # @param [Puppeteer::HTTPRequest] request
92
92
  def handle_request(request)
93
93
  return if request.frame != @frame || !request.navigation_request?
94
94
  @navigation_request = request
@@ -103,7 +103,7 @@ class Puppeteer::LifecycleWatcher
103
103
  check_lifecycle_complete
104
104
  end
105
105
 
106
- # @return [Puppeteer::Response]
106
+ # @return [Puppeteer::HTTPResponse]
107
107
  def navigation_response
108
108
  if_present(@navigation_request) do |request|
109
109
  request.response
@@ -251,9 +251,14 @@ class Puppeteer::NetworkManager
251
251
  end
252
252
  end
253
253
  frame = if_present(event['frameId']) { |frame_id| @frame_manager.frame(frame_id) }
254
- request = Puppeteer::Request.new(@client, frame, interception_id, @user_request_interception_enabled, event, redirect_chain)
254
+ request = Puppeteer::HTTPRequest.new(@client, frame, interception_id, @user_request_interception_enabled, event, redirect_chain)
255
255
  @request_id_to_request[event['requestId']] = request
256
256
  emit_event(NetworkManagerEmittedEvents::Request, request)
257
+ begin
258
+ request.finalize_interceptions
259
+ rescue => err
260
+ debug_puts(err)
261
+ end
257
262
  end
258
263
 
259
264
  private def handle_request_served_from_cache(event)
@@ -262,13 +267,13 @@ class Puppeteer::NetworkManager
262
267
  end
263
268
  end
264
269
 
265
- # @param request [Puppeteer::Request]
270
+ # @param request [Puppeteer::HTTPRequest]
266
271
  # @param response_payload [Hash]
267
272
  private def handle_request_redirect(request, response_payload)
268
- response = Puppeteer::Response.new(@client, request, response_payload)
273
+ response = Puppeteer::HTTPResponse.new(@client, request, response_payload)
269
274
  request.internal.response = response
270
275
  request.internal.redirect_chain << request
271
- response.internal.body_loaded_promise.reject(Puppeteer::Response::Redirected.new)
276
+ response.internal.body_loaded_promise.reject(Puppeteer::HTTPResponse::Redirected.new)
272
277
  @request_id_to_request.delete(request.internal.request_id)
273
278
  @attempted_authentications.delete(request.internal.interception_id)
274
279
  emit_event(NetworkManagerEmittedEvents::Response, response)
@@ -281,7 +286,7 @@ class Puppeteer::NetworkManager
281
286
  # FileUpload sends a response without a matching request.
282
287
  return unless request
283
288
 
284
- response = Puppeteer::Response.new(@client, request, event['response'])
289
+ response = Puppeteer::HTTPResponse.new(@client, request, event['response'])
285
290
  request.internal.response = response
286
291
  emit_event(NetworkManagerEmittedEvents::Response, response)
287
292
  end
@@ -34,7 +34,7 @@ class Puppeteer::Page
34
34
  @type ||= 'png'
35
35
 
36
36
  if options[:quality]
37
- unless @type == 'jpeg'
37
+ if @type != 'jpeg' && @type != 'webp'
38
38
  raise ArgumentError.new("options.quality is unsupported for the #{@type} screenshots")
39
39
  end
40
40
  unless options[:quality].is_a?(Numeric)