puppeteer-ruby 0.34.3 → 0.37.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -43
  3. data/README.md +78 -11
  4. data/docs/api_coverage.md +27 -6
  5. data/lib/puppeteer/browser.rb +2 -8
  6. data/lib/puppeteer/browser_context.rb +1 -0
  7. data/lib/puppeteer/concurrent_ruby_utils.rb +2 -2
  8. data/lib/puppeteer/coverage.rb +11 -2
  9. data/lib/puppeteer/define_async_method.rb +1 -1
  10. data/lib/puppeteer/devices.rb +132 -0
  11. data/lib/puppeteer/dom_world.rb +10 -9
  12. data/lib/puppeteer/element_handle/offset.rb +28 -0
  13. data/lib/puppeteer/element_handle/point.rb +11 -0
  14. data/lib/puppeteer/element_handle.rb +68 -7
  15. data/lib/puppeteer/frame.rb +3 -2
  16. data/lib/puppeteer/js_coverage.rb +28 -7
  17. data/lib/puppeteer/launcher/chrome.rb +64 -4
  18. data/lib/puppeteer/launcher/firefox.rb +48 -4
  19. data/lib/puppeteer/launcher/launch_options.rb +2 -1
  20. data/lib/puppeteer/launcher.rb +0 -1
  21. data/lib/puppeteer/mouse.rb +54 -1
  22. data/lib/puppeteer/network_condition.rb +12 -0
  23. data/lib/puppeteer/network_conditions.rb +24 -0
  24. data/lib/puppeteer/network_manager.rb +47 -11
  25. data/lib/puppeteer/page/metrics.rb +49 -0
  26. data/lib/puppeteer/page/screenshot_options.rb +3 -1
  27. data/lib/puppeteer/page.rb +147 -93
  28. data/lib/puppeteer/puppeteer.rb +10 -2
  29. data/lib/puppeteer/timeout_helper.rb +22 -0
  30. data/lib/puppeteer/tracing.rb +6 -1
  31. data/lib/puppeteer/version.rb +1 -1
  32. data/lib/puppeteer/web_socket.rb +1 -0
  33. data/lib/puppeteer-ruby.rb +2 -0
  34. data/lib/puppeteer.rb +15 -12
  35. data/puppeteer-ruby.gemspec +1 -1
  36. metadata +10 -7
  37. data/Dockerfile +0 -9
  38. data/docker-compose.yml +0 -34
  39. data/lib/puppeteer/launcher/base.rb +0 -66
@@ -1,3 +1,5 @@
1
+ require_relative './device'
2
+
1
3
  Puppeteer::DEVICES = Hash[
2
4
  [
3
5
  {
@@ -156,6 +158,84 @@ Puppeteer::DEVICES = Hash[
156
158
  isLandscape: true,
157
159
  },
158
160
  },
161
+ {
162
+ name: 'Galaxy S8',
163
+ userAgent:
164
+ 'Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36',
165
+ viewport: {
166
+ width: 360,
167
+ height: 740,
168
+ deviceScaleFactor: 3,
169
+ isMobile: true,
170
+ hasTouch: true,
171
+ isLandscape: false,
172
+ },
173
+ },
174
+ {
175
+ name: 'Galaxy S8 landscape',
176
+ userAgent:
177
+ 'Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36',
178
+ viewport: {
179
+ width: 740,
180
+ height: 360,
181
+ deviceScaleFactor: 3,
182
+ isMobile: true,
183
+ hasTouch: true,
184
+ isLandscape: true,
185
+ },
186
+ },
187
+ {
188
+ name: 'Galaxy S9+',
189
+ userAgent:
190
+ 'Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36',
191
+ viewport: {
192
+ width: 320,
193
+ height: 658,
194
+ deviceScaleFactor: 4.5,
195
+ isMobile: true,
196
+ hasTouch: true,
197
+ isLandscape: false,
198
+ },
199
+ },
200
+ {
201
+ name: 'Galaxy S9+ landscape',
202
+ userAgent:
203
+ 'Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36',
204
+ viewport: {
205
+ width: 658,
206
+ height: 320,
207
+ deviceScaleFactor: 4.5,
208
+ isMobile: true,
209
+ hasTouch: true,
210
+ isLandscape: true,
211
+ },
212
+ },
213
+ {
214
+ name: 'Galaxy Tab S4',
215
+ userAgent:
216
+ 'Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Safari/537.36',
217
+ viewport: {
218
+ width: 712,
219
+ height: 1138,
220
+ deviceScaleFactor: 2.25,
221
+ isMobile: true,
222
+ hasTouch: true,
223
+ isLandscape: false,
224
+ },
225
+ },
226
+ {
227
+ name: 'Galaxy Tab S4 landscape',
228
+ userAgent:
229
+ 'Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Safari/537.36',
230
+ viewport: {
231
+ width: 1138,
232
+ height: 712,
233
+ deviceScaleFactor: 2.25,
234
+ isMobile: true,
235
+ hasTouch: true,
236
+ isLandscape: true,
237
+ },
238
+ },
159
239
  {
160
240
  name: 'iPad',
161
241
  userAgent:
@@ -1001,6 +1081,58 @@ Puppeteer::DEVICES = Hash[
1001
1081
  isLandscape: true,
1002
1082
  },
1003
1083
  },
1084
+ {
1085
+ name: 'Pixel 3',
1086
+ userAgent:
1087
+ 'Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.158 Mobile Safari/537.36',
1088
+ viewport: {
1089
+ width: 393,
1090
+ height: 786,
1091
+ deviceScaleFactor: 2.75,
1092
+ isMobile: true,
1093
+ hasTouch: true,
1094
+ isLandscape: false,
1095
+ },
1096
+ },
1097
+ {
1098
+ name: 'Pixel 3 landscape',
1099
+ userAgent:
1100
+ 'Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.158 Mobile Safari/537.36',
1101
+ viewport: {
1102
+ width: 786,
1103
+ height: 393,
1104
+ deviceScaleFactor: 2.75,
1105
+ isMobile: true,
1106
+ hasTouch: true,
1107
+ isLandscape: true,
1108
+ },
1109
+ },
1110
+ {
1111
+ name: 'Pixel 4',
1112
+ userAgent:
1113
+ 'Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Mobile Safari/537.36',
1114
+ viewport: {
1115
+ width: 353,
1116
+ height: 745,
1117
+ deviceScaleFactor: 3,
1118
+ isMobile: true,
1119
+ hasTouch: true,
1120
+ isLandscape: false,
1121
+ },
1122
+ },
1123
+ {
1124
+ name: 'Pixel 4 landscape',
1125
+ userAgent:
1126
+ 'Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Mobile Safari/537.36',
1127
+ viewport: {
1128
+ width: 745,
1129
+ height: 353,
1130
+ deviceScaleFactor: 3,
1131
+ isMobile: true,
1132
+ hasTouch: true,
1133
+ isLandscape: true,
1134
+ },
1135
+ },
1004
1136
  ].map do |json|
1005
1137
  [
1006
1138
  json[:name],
@@ -233,12 +233,13 @@ class Puppeteer::DOMWorld
233
233
  # @param url [String?]
234
234
  # @param path [String?]
235
235
  # @param content [String?]
236
+ # @param id [String?]
236
237
  # @param type [String?]
237
- def add_script_tag(url: nil, path: nil, content: nil, type: nil)
238
+ def add_script_tag(url: nil, path: nil, content: nil, id: nil, type: nil)
238
239
  if url
239
240
  begin
240
241
  return execution_context.
241
- evaluate_handle(ADD_SCRIPT_URL, url, type || '').
242
+ evaluate_handle(ADD_SCRIPT_URL, url, id, type || '').
242
243
  as_element
243
244
  rescue Puppeteer::ExecutionContext::EvaluationError # for Chrome
244
245
  raise "Loading script from #{url} failed"
@@ -251,13 +252,13 @@ class Puppeteer::DOMWorld
251
252
  contents = File.read(path)
252
253
  contents += "//# sourceURL=#{path.gsub(/\n/, '')}"
253
254
  return execution_context.
254
- evaluate_handle(ADD_SCRIPT_CONTENT, contents, type || '').
255
+ evaluate_handle(ADD_SCRIPT_CONTENT, contents, id, type || 'text/javascript').
255
256
  as_element
256
257
  end
257
258
 
258
259
  if content
259
260
  return execution_context.
260
- evaluate_handle(ADD_SCRIPT_CONTENT, content, type || '').
261
+ evaluate_handle(ADD_SCRIPT_CONTENT, content, id, type || 'text/javascript').
261
262
  as_element
262
263
  end
263
264
 
@@ -265,11 +266,11 @@ class Puppeteer::DOMWorld
265
266
  end
266
267
 
267
268
  ADD_SCRIPT_URL = <<~JAVASCRIPT
268
- async (url, type) => {
269
+ async (url, id, type) => {
269
270
  const script = document.createElement('script');
270
271
  script.src = url;
271
- if (type)
272
- script.type = type;
272
+ if (id) script.id = id;
273
+ if (type) script.type = type;
273
274
  const promise = new Promise((res, rej) => {
274
275
  script.onload = res;
275
276
  script.onerror = rej;
@@ -281,11 +282,11 @@ class Puppeteer::DOMWorld
281
282
  JAVASCRIPT
282
283
 
283
284
  ADD_SCRIPT_CONTENT = <<~JAVASCRIPT
284
- (content, type) => {
285
- if (type === undefined) type = 'text/javascript';
285
+ (content, id, type) => {
286
286
  const script = document.createElement('script');
287
287
  script.type = type;
288
288
  script.text = content;
289
+ if (id) script.id = id;
289
290
  let error = null;
290
291
  script.onerror = e => error = e;
291
292
  document.head.appendChild(script);
@@ -0,0 +1,28 @@
1
+ class Puppeteer::ElementHandle < Puppeteer::JSHandle
2
+ # A class to represent (x, y)-offset coordinates
3
+ class Offset
4
+ def initialize(x:, y:)
5
+ @x = x
6
+ @y = y
7
+ end
8
+
9
+ def self.from(offset)
10
+ case offset
11
+ when nil
12
+ nil
13
+ when Hash
14
+ if offset[:x] && offset[:y]
15
+ Offset.new(x: offset[:x], y: offset[:y])
16
+ else
17
+ raise ArgumentError.new('offset parameter must have x, y coordinates')
18
+ end
19
+ when Offset
20
+ offset
21
+ else
22
+ raise ArgumentError.new('Offset.from(Hash|Offset)')
23
+ end
24
+ end
25
+
26
+ attr_reader :x, :y
27
+ end
28
+ end
@@ -21,6 +21,17 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
21
21
  )
22
22
  end
23
23
 
24
+ def ==(other)
25
+ case other
26
+ when Hash
27
+ @x == other[:x] && @y == other[:y]
28
+ when Point
29
+ @x == other.x && @y == other.y
30
+ else
31
+ super
32
+ end
33
+ end
34
+
24
35
  attr_reader :x, :y
25
36
  end
26
37
  end
@@ -1,5 +1,6 @@
1
1
  require_relative './element_handle/bounding_box'
2
2
  require_relative './element_handle/box_model'
3
+ require_relative './element_handle/offset'
3
4
  require_relative './element_handle/point'
4
5
 
5
6
  class Puppeteer::ElementHandle < Puppeteer::JSHandle
@@ -79,7 +80,9 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
79
80
  end
80
81
  end
81
82
 
82
- def clickable_point
83
+ def clickable_point(offset = nil)
84
+ offset_param = Offset.from(offset)
85
+
83
86
  result =
84
87
  begin
85
88
  @remote_object.content_quads(@client)
@@ -105,6 +108,19 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
105
108
  raise ElementNotVisibleError.new
106
109
  end
107
110
 
111
+ if offset_param
112
+ # Return the point of the first quad identified by offset.
113
+ quad = quads.first
114
+ min_x = quad.map(&:x).min
115
+ min_y = quad.map(&:y).min
116
+ if min_x && min_y
117
+ return Point.new(
118
+ x: min_x + offset_param.x,
119
+ y: min_y + offset_param.y,
120
+ )
121
+ end
122
+ end
123
+
108
124
  # Return the middle point of the first quad.
109
125
  quads.first.reduce(:+) / 4
110
126
  end
@@ -139,14 +155,56 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
139
155
  # @param delay [Number]
140
156
  # @param button [String] "left"|"right"|"middle"
141
157
  # @param click_count [Number]
142
- def click(delay: nil, button: nil, click_count: nil)
158
+ # @param offset [Hash]
159
+ def click(delay: nil, button: nil, click_count: nil, offset: nil)
143
160
  scroll_into_view_if_needed
144
- point = clickable_point
161
+ point = clickable_point(offset)
145
162
  @page.mouse.click(point.x, point.y, delay: delay, button: button, click_count: click_count)
146
163
  end
147
164
 
148
165
  define_async_method :async_click
149
166
 
167
+ class DragInterceptionNotEnabledError < StandardError
168
+ def initialize
169
+ super('Drag Interception is not enabled!')
170
+ end
171
+ end
172
+
173
+ def drag(x:, y:)
174
+ unless @page.drag_interception_enabled?
175
+ raise DragInterceptionNotEnabledError.new
176
+ end
177
+ scroll_into_view_if_needed
178
+ start = clickable_point
179
+ @page.mouse.drag(start, Point.new(x: x, y: y))
180
+ end
181
+
182
+ def drag_enter(data)
183
+ scroll_into_view_if_needed
184
+ target = clickable_point
185
+ @page.mouse.drag_enter(target, data)
186
+ end
187
+
188
+ def drag_over(data)
189
+ scroll_into_view_if_needed
190
+ target = clickable_point
191
+ @page.mouse.drag_over(target, data)
192
+ end
193
+
194
+ def drop(data)
195
+ scroll_into_view_if_needed
196
+ target = clickable_point
197
+ @page.mouse.drop(target, data)
198
+ end
199
+
200
+ # @param target [ElementHandle]
201
+ def drag_and_drop(target, delay: nil)
202
+ scroll_into_view_if_needed
203
+ start_point = clickable_point
204
+ target_point = target.clickable_point
205
+ @page.mouse.drag_and_drop(start_point, target_point, delay: delay)
206
+ end
207
+
150
208
  # @return [Array<String>]
151
209
  def select(*values)
152
210
  if nonstring = values.find { |value| !value.is_a?(String) }
@@ -395,10 +453,12 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
395
453
  define_async_method :async_Sx
396
454
 
397
455
  # in JS, #isIntersectingViewport.
456
+ # @param threshold [Float|nil]
398
457
  # @return [Boolean]
399
- def intersecting_viewport?
458
+ def intersecting_viewport?(threshold: nil)
459
+ option_threshold = threshold || 0
400
460
  js = <<~JAVASCRIPT
401
- async element => {
461
+ async (element, threshold) => {
402
462
  const visibleRatio = await new Promise(resolve => {
403
463
  const observer = new IntersectionObserver(entries => {
404
464
  resolve(entries[0].intersectionRatio);
@@ -406,11 +466,12 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
406
466
  });
407
467
  observer.observe(element);
408
468
  });
409
- return visibleRatio > 0;
469
+ if (threshold === 1) return visibleRatio === 1;
470
+ else return visibleRatio > threshold;
410
471
  }
411
472
  JAVASCRIPT
412
473
 
413
- evaluate(js)
474
+ evaluate(js, option_threshold)
414
475
  end
415
476
 
416
477
  # @param quad [Array<Point>]
@@ -157,8 +157,9 @@ class Puppeteer::Frame
157
157
  # @param path [String?]
158
158
  # @param content [String?]
159
159
  # @param type [String?]
160
- def add_script_tag(url: nil, path: nil, content: nil, type: nil)
161
- @main_world.add_script_tag(url: url, path: path, content: content, type: type)
160
+ # @param id [String?]
161
+ def add_script_tag(url: nil, path: nil, content: nil, type: nil, id: nil)
162
+ @main_world.add_script_tag(url: url, path: path, content: content, type: type, id: id)
162
163
  end
163
164
 
164
165
  # @param url [String?]
@@ -12,6 +12,14 @@ class Puppeteer::JSCoverage
12
12
  attr_reader :url, :ranges, :text
13
13
  end
14
14
 
15
+ class ItemWithRawScriptCoverage < Item
16
+ def initialize(url:, ranges:, text:, raw_script_coverage:)
17
+ super(url: url, ranges: ranges, text: text)
18
+ @raw_script_coverage = raw_script_coverage
19
+ end
20
+ attr_reader :raw_script_coverage
21
+ end
22
+
15
23
  # @param client [Puppeteer::CDPSession]
16
24
  def initialize(client)
17
25
  @client = client
@@ -20,7 +28,10 @@ class Puppeteer::JSCoverage
20
28
  @script_sources = {}
21
29
  end
22
30
 
23
- def start(reset_on_navigation: nil, report_anonymous_scripts: nil)
31
+ def start(
32
+ reset_on_navigation: nil,
33
+ report_anonymous_scripts: nil,
34
+ include_raw_script_coverage: nil)
24
35
  raise 'JSCoverage is already enabled' if @enabled
25
36
 
26
37
  @reset_on_navigation =
@@ -30,6 +41,7 @@ class Puppeteer::JSCoverage
30
41
  true
31
42
  end
32
43
  @report_anonymous_scripts = report_anonymous_scripts || false
44
+ @include_raw_script_coverage = include_raw_script_coverage || false
33
45
  @enabled = true
34
46
  @script_urls.clear
35
47
  @script_sources.clear
@@ -43,7 +55,7 @@ class Puppeteer::JSCoverage
43
55
  await_all(
44
56
  @client.async_send_message('Profiler.enable'),
45
57
  @client.async_send_message('Profiler.startPreciseCoverage',
46
- callCount: false,
58
+ callCount: @include_raw_script_coverage,
47
59
  detailed: true,
48
60
  ),
49
61
  @client.async_send_message('Debugger.enable'),
@@ -107,11 +119,20 @@ class Puppeteer::JSCoverage
107
119
  end
108
120
  end
109
121
 
110
- coverage << Item.new(
111
- url: url,
112
- ranges: convert_to_disjoint_ranges(flatten_ranges),
113
- text: text,
114
- )
122
+ if @include_raw_script_coverage
123
+ coverage << ItemWithRawScriptCoverage.new(
124
+ url: url,
125
+ ranges: convert_to_disjoint_ranges(flatten_ranges),
126
+ text: text,
127
+ raw_script_coverage: entry,
128
+ )
129
+ else
130
+ coverage << Item.new(
131
+ url: url,
132
+ ranges: convert_to_disjoint_ranges(flatten_ranges),
133
+ text: text,
134
+ )
135
+ end
115
136
  end
116
137
 
117
138
  coverage
@@ -2,7 +2,13 @@ require 'tmpdir'
2
2
 
3
3
  # https://github.com/puppeteer/puppeteer/blob/main/src/node/Launcher.ts
4
4
  module Puppeteer::Launcher
5
- class Chrome < Base
5
+ class Chrome
6
+ def initialize(project_root:, preferred_revision:, is_puppeteer_core:)
7
+ @project_root = project_root
8
+ @preferred_revision = preferred_revision
9
+ @is_puppeteer_core = is_puppeteer_core
10
+ end
11
+
6
12
  # @param {!(Launcher.LaunchOptions & Launcher.ChromeArgOptions & Launcher.BrowserOptions)=} options
7
13
  # @return {!Promise<!Browser>}
8
14
  def launch(options = {})
@@ -38,7 +44,12 @@ module Puppeteer::Launcher
38
44
  chrome_arguments << "--user-data-dir=#{temporary_user_data_dir}"
39
45
  end
40
46
 
41
- chrome_executable = @launch_options.executable_path || resolve_executable_path
47
+ chrome_executable =
48
+ if @launch_options.channel
49
+ executable_path_for_channel(@launch_options.channel.to_s)
50
+ else
51
+ @launch_options.executable_path || executable_path_for_channel('chrome')
52
+ end
42
53
  use_pipe = chrome_arguments.include?('--remote-debugging-pipe')
43
54
  runner = Puppeteer::BrowserRunner.new(chrome_executable, chrome_arguments, temporary_user_data_dir)
44
55
  runner.start(
@@ -201,8 +212,57 @@ module Puppeteer::Launcher
201
212
  end
202
213
 
203
214
  # @return {string}
204
- def executable_path
205
- resolve_executable_path
215
+ def executable_path(channel: nil)
216
+ if channel
217
+ executable_path_for_channel(channel.to_s)
218
+ else
219
+ executable_path_for_channel('chrome')
220
+ end
221
+ end
222
+
223
+ CHROMIUM_CHANNELS = {
224
+ windows: {
225
+ 'chrome' => "#{ENV['PROGRAMFILES']}\\Google\\Chrome\\Application\\chrome.exe",
226
+ 'chrome-beta' => "#{ENV['PROGRAMFILES']}\\Google\\Chrome Beta\\Application\\chrome.exe",
227
+ 'chrome-canary' => "#{ENV['PROGRAMFILES']}\\Google\\Chrome SxS\\Application\\chrome.exe",
228
+ 'chrome-dev' => "#{ENV['PROGRAMFILES']}\\Google\\Chrome Dev\\Application\\chrome.exe",
229
+ 'msedge' => "#{ENV['PROGRAMFILES(X86)']}\\Microsoft\\Edge\\Application\\msedge.exe",
230
+ },
231
+ darwin: {
232
+ 'chrome' => '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
233
+ 'chrome-beta' => '/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta',
234
+ 'chrome-canary' => '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
235
+ 'chrome-dev' => '/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev',
236
+ 'msedge' => '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
237
+ },
238
+ linux: {
239
+ 'chrome' => '/opt/google/chrome/chrome',
240
+ 'chrome-beta' => '/opt/google/chrome-beta/chrome',
241
+ 'chrome-dev' => '/opt/google/chrome-unstable/chrome',
242
+ },
243
+ }.freeze
244
+
245
+ # @param channel [String]
246
+ private def executable_path_for_channel(channel)
247
+ chrome_path_map =
248
+ if Puppeteer.env.windows?
249
+ CHROMIUM_CHANNELS[:windows]
250
+ elsif Puppeteer.env.darwin?
251
+ CHROMIUM_CHANNELS[:darwin]
252
+ else
253
+ CHROMIUM_CHANNELS[:linux]
254
+ end
255
+
256
+ chrome_path = chrome_path_map[channel]
257
+ unless chrome_path
258
+ raise ArgumentError.new("Invalid channel: '#{channel}'. Allowed channel is #{chrome_path_map.keys}")
259
+ end
260
+
261
+ unless File.exist?(chrome_path)
262
+ raise "#{channel} is not installed on this system.\nExpected path: #{chrome_path}"
263
+ end
264
+
265
+ chrome_path
206
266
  end
207
267
 
208
268
  def product
@@ -2,7 +2,13 @@ require 'tmpdir'
2
2
 
3
3
  # https://github.com/puppeteer/puppeteer/blob/main/src/node/Launcher.ts
4
4
  module Puppeteer::Launcher
5
- class Firefox < Base
5
+ class Firefox
6
+ def initialize(project_root:, preferred_revision:, is_puppeteer_core:)
7
+ @project_root = project_root
8
+ @preferred_revision = preferred_revision
9
+ @is_puppeteer_core = is_puppeteer_core
10
+ end
11
+
6
12
  # @param {!(Launcher.LaunchOptions & Launcher.ChromeArgOptions & Launcher.BrowserOptions)=} options
7
13
  # @return {!Promise<!Browser>}
8
14
  def launch(options = {})
@@ -32,7 +38,12 @@ module Puppeteer::Launcher
32
38
  firefox_arguments << temporary_user_data_dir
33
39
  end
34
40
 
35
- firefox_executable = @launch_options.executable_path || resolve_executable_path
41
+ firefox_executable =
42
+ if @launch_options.channel
43
+ executable_path_for_channel(@launch_options.channel.to_s)
44
+ else
45
+ @launch_options.executable_path || executable_path_for_channel('nightly')
46
+ end
36
47
  runner = Puppeteer::BrowserRunner.new(firefox_executable, firefox_arguments, temporary_user_data_dir)
37
48
  runner.start(
38
49
  handle_SIGHUP: @launch_options.handle_SIGHUP?,
@@ -123,8 +134,41 @@ module Puppeteer::Launcher
123
134
  end
124
135
 
125
136
  # @return {string}
126
- def executable_path
127
- resolve_executable_path
137
+ def executable_path(channel: nil)
138
+ if channel
139
+ executable_path_for_channel(channel.to_s)
140
+ else
141
+ executable_path_for_channel('firefox')
142
+ end
143
+ end
144
+
145
+ FIREFOX_EXECUTABLE_PATHS = {
146
+ windows: "#{ENV['PROGRAMFILES']}\\Firefox Nightly\\firefox.exe",
147
+ darwin: '/Applications/Firefox Nightly.app/Contents/MacOS/firefox',
148
+ linux: '/usr/bin/firefox',
149
+ }.freeze
150
+
151
+ # @param channel [String]
152
+ private def executable_path_for_channel(channel)
153
+ allowed = ['firefox', 'firefox-nightly', 'nightly']
154
+ unless allowed.include?(channel)
155
+ raise ArgumentError.new("Invalid channel: '#{channel}'. Allowed channel is #{allowed}")
156
+ end
157
+
158
+ firefox_path =
159
+ if Puppeteer.env.windows?
160
+ FIREFOX_EXECUTABLE_PATHS[:windows]
161
+ elsif Puppeteer.env.darwin?
162
+ FIREFOX_EXECUTABLE_PATHS[:darwin]
163
+ else
164
+ FIREFOX_EXECUTABLE_PATHS[:linux]
165
+ end
166
+
167
+ unless File.exist?(firefox_path)
168
+ raise "Nightly version of Firefox is not installed on this system.\nExpected path: #{firefox_path}"
169
+ end
170
+
171
+ firefox_path
128
172
  end
129
173
 
130
174
  def product
@@ -32,6 +32,7 @@ module Puppeteer::Launcher
32
32
  # @property {!Object<string, string | undefined>=} env
33
33
  # @property {boolean=} pipe
34
34
  def initialize(options)
35
+ @channel = options[:channel]
35
36
  @executable_path = options[:executable_path]
36
37
  @ignore_default_args = options[:ignore_default_args] || false
37
38
  @handle_SIGINT = options[:handle_SIGINT] || true
@@ -43,7 +44,7 @@ module Puppeteer::Launcher
43
44
  @pipe = options[:pipe] || false
44
45
  end
45
46
 
46
- attr_reader :executable_path, :ignore_default_args, :timeout, :env
47
+ attr_reader :channel, :executable_path, :ignore_default_args, :timeout, :env
47
48
 
48
49
  def handle_SIGINT?
49
50
  @handle_SIGINT
@@ -1,4 +1,3 @@
1
- require_relative './launcher/base'
2
1
  require_relative './launcher/browser_options'
3
2
  require_relative './launcher/chrome'
4
3
  require_relative './launcher/chrome_arg_options'