puppeteer-ruby 0.37.1 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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)