puppeteer-ruby 0.0.17 → 0.0.22

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.
@@ -0,0 +1,34 @@
1
+ class Puppeteer::Dialog
2
+ def initialize(client, type:, message:, default_value:)
3
+ @client = client
4
+ @type = type
5
+ @message = message
6
+ @default_value = default_value || ''
7
+ end
8
+
9
+ attr_reader :type, :message, :default_value
10
+
11
+ # @param prompt_text - optional text that will be entered in the dialog
12
+ # prompt. Has no effect if the dialog's type is not `prompt`.
13
+ #
14
+ # @returns A promise that resolves when the dialog has been accepted.
15
+ def accept(prompt_text = nil)
16
+ if @handled
17
+ raise 'Cannot accept dialog which is already handled!'
18
+ end
19
+ @handled = true
20
+ @client.send_message('Page.handleJavaScriptDialog', {
21
+ accept: true,
22
+ promptText: prompt_text,
23
+ }.compact)
24
+ end
25
+
26
+ # @returns A promise which will resolve once the dialog has been dismissed
27
+ def dismiss
28
+ if @handled
29
+ raise 'Cannot accept dialog which is already handled!'
30
+ end
31
+ @handled = true
32
+ @client.send_message('Page.handleJavaScriptDialog', accept: false)
33
+ end
34
+ end
@@ -402,6 +402,26 @@ class Puppeteer::DOMWorld
402
402
  # return new WaitTask(this, pageFunction, 'function', polling, timeout, ...args).promise;
403
403
  # }
404
404
 
405
+ # @param page_function [String]
406
+ # @param args [Array]
407
+ # @param polling [Integer|String]
408
+ # @param timeout [Integer]
409
+ # @return [Puppeteer::JSHandle]
410
+ def wait_for_function(page_function, args: [], polling: nil, timeout: nil)
411
+ option_polling = polling || 'raf'
412
+ option_timeout = timeout || @timeout_settings.timeout
413
+
414
+ Puppeteer::WaitTask.new(
415
+ dom_world: self,
416
+ predicate_body: page_function,
417
+ title: 'function',
418
+ polling: option_polling,
419
+ timeout: option_timeout,
420
+ args: args,
421
+ ).await_promise
422
+ end
423
+
424
+
405
425
  # @return [String]
406
426
  def title
407
427
  evaluate('() => document.title')
@@ -427,7 +447,7 @@ class Puppeteer::DOMWorld
427
447
 
428
448
  wait_task = Puppeteer::WaitTask.new(
429
449
  dom_world: self,
430
- predicate_body: "return (#{PREDICATE})(...args)",
450
+ predicate_body: PREDICATE,
431
451
  title: title,
432
452
  polling: polling,
433
453
  timeout: option_timeout,
@@ -3,6 +3,7 @@ require_relative './element_handle/box_model'
3
3
  require_relative './element_handle/point'
4
4
 
5
5
  class Puppeteer::ElementHandle < Puppeteer::JSHandle
6
+ include Puppeteer::DebugPrint
6
7
  include Puppeteer::IfPresent
7
8
  using Puppeteer::DefineAsyncMethod
8
9
 
@@ -42,7 +43,24 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
42
43
  if (element.nodeType !== Node.ELEMENT_NODE)
43
44
  return 'Node is not of type HTMLElement';
44
45
 
45
- element.scrollIntoViewIfNeeded({block: 'center', inline: 'center', behavior: 'instant'});
46
+ if (element.scrollIntoViewIfNeeded) {
47
+ element.scrollIntoViewIfNeeded({block: 'center', inline: 'center', behavior: 'instant'});
48
+ } else {
49
+ // force-scroll if page's javascript is disabled.
50
+ if (!pageJavascriptEnabled) {
51
+ element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
52
+ return false;
53
+ }
54
+ const visibleRatio = await new Promise(resolve => {
55
+ const observer = new IntersectionObserver(entries => {
56
+ resolve(entries[0].intersectionRatio);
57
+ observer.disconnect();
58
+ });
59
+ observer.observe(element);
60
+ });
61
+ if (visibleRatio !== 1.0)
62
+ element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
63
+ }
46
64
  return false;
47
65
  }
48
66
  JAVASCRIPT
@@ -62,7 +80,14 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
62
80
  end
63
81
 
64
82
  def clickable_point
65
- result = @remote_object.content_quads(@client)
83
+ result =
84
+ begin
85
+ @remote_object.content_quads(@client)
86
+ rescue => err
87
+ debug_puts(err)
88
+ nil
89
+ end
90
+
66
91
  if !result || result["quads"].empty?
67
92
  raise ElementNotVisibleError.new
68
93
  end
@@ -380,21 +405,24 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
380
405
 
381
406
  define_async_method :async_Sx
382
407
 
383
- # /**
384
- # * @returns {!Promise<boolean>}
385
- # */
386
- # isIntersectingViewport() {
387
- # return this.evaluate(async element => {
388
- # const visibleRatio = await new Promise(resolve => {
389
- # const observer = new IntersectionObserver(entries => {
390
- # resolve(entries[0].intersectionRatio);
391
- # observer.disconnect();
392
- # });
393
- # observer.observe(element);
394
- # });
395
- # return visibleRatio > 0;
396
- # });
397
- # }
408
+ # in JS, #isIntersectingViewport.
409
+ # @return [Boolean]
410
+ def intersecting_viewport?
411
+ js = <<~JAVASCRIPT
412
+ async element => {
413
+ const visibleRatio = await new Promise(resolve => {
414
+ const observer = new IntersectionObserver(entries => {
415
+ resolve(entries[0].intersectionRatio);
416
+ observer.disconnect();
417
+ });
418
+ observer.observe(element);
419
+ });
420
+ return visibleRatio > 0;
421
+ }
422
+ JAVASCRIPT
423
+
424
+ evaluate(js)
425
+ end
398
426
 
399
427
  # @param quad [Array<Point>]
400
428
  private def compute_quad_area(quad)
@@ -9,6 +9,11 @@ class Puppeteer::Env
9
9
  def ci?
10
10
  ['1', 'true'].include?(ENV['CI'].to_s)
11
11
  end
12
+
13
+ # check if running on macOS
14
+ def darwin?
15
+ RUBY_PLATFORM.include?('darwin')
16
+ end
12
17
  end
13
18
 
14
19
  class Puppeteer
@@ -142,7 +142,7 @@ class Puppeteer::Frame
142
142
  end
143
143
 
144
144
  def child_frames
145
- @child_frames.dup
145
+ @child_frames.to_a
146
146
  end
147
147
 
148
148
  def detached?
@@ -225,6 +225,11 @@ class Puppeteer::Frame
225
225
 
226
226
  define_async_method :async_wait_for_selector
227
227
 
228
+ # @param milliseconds [Integer] the number of milliseconds to wait.
229
+ def wait_for_timeout(milliseconds)
230
+ sleep(milliseconds / 1000.0)
231
+ end
232
+
228
233
  # @param xpath [String]
229
234
  # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
230
235
  # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
@@ -242,12 +247,13 @@ class Puppeteer::Frame
242
247
 
243
248
  define_async_method :async_wait_for_xpath
244
249
 
245
- # @param {Function|string} pageFunction
246
- # @param {!{polling?: string|number, timeout?: number}=} options
247
- # @param {!Array<*>} args
248
- # @return {!Promise<!Puppeteer.JSHandle>}
249
- def wait_for_function(page_function, options = {}, *args)
250
- @main_world.wait_for_function(page_function, options, *args)
250
+ # @param page_function [String]
251
+ # @param args [Integer|Array]
252
+ # @param polling [String]
253
+ # @param timeout [Integer]
254
+ # @return [Puppeteer::JSHandle]
255
+ def wait_for_function(page_function, args: [], polling: nil, timeout: nil)
256
+ @main_world.wait_for_function(page_function, args: args, polling: polling, timeout: timeout)
251
257
  end
252
258
 
253
259
  define_async_method :async_wait_for_function
@@ -260,9 +266,7 @@ class Puppeteer::Frame
260
266
  # @param frame_payload [Hash]
261
267
  def navigated(frame_payload)
262
268
  @name = frame_payload['name']
263
- # TODO(lushnikov): remove this once requestInterception has loaderId exposed.
264
- @navigation_url = frame_payload['url']
265
- @url = frame_payload['url']
269
+ @url = "#{frame_payload['url']}#{frame_payload['urlFragment']}"
266
270
 
267
271
  # Ensure loaderId updated.
268
272
  # The order of [Page.lifecycleEvent name="init"] and [Page.frameNavigated] is random... for some reason...
@@ -1,5 +1,6 @@
1
1
  class Puppeteer::JSHandle
2
2
  using Puppeteer::DefineAsyncMethod
3
+ include Puppeteer::IfPresent
3
4
 
4
5
  # @param context [Puppeteer::ExecutionContext]
5
6
  # @param remote_object [Puppeteer::RemoteObject]
@@ -57,21 +58,29 @@ class Puppeteer::JSHandle
57
58
 
58
59
  define_async_method :async_evaluate_handle
59
60
 
60
- # /**
61
- # * @param {string} propertyName
62
- # * @return {!Promise<?JSHandle>}
63
- # */
64
- # async getProperty(propertyName) {
65
- # const objectHandle = await this.evaluateHandle((object, propertyName) => {
66
- # const result = {__proto__: null};
67
- # result[propertyName] = object[propertyName];
68
- # return result;
69
- # }, propertyName);
70
- # const properties = await objectHandle.getProperties();
71
- # const result = properties.get(propertyName) || null;
72
- # await objectHandle.dispose();
73
- # return result;
74
- # }
61
+ # getProperty(propertyName) in JavaScript
62
+ # @param name [String]
63
+ # @return [Puppeteer::JSHandle]
64
+ def property(name)
65
+ js = <<~JAVASCRIPT
66
+ (object, propertyName) => {
67
+ const result = {__proto__: null};
68
+ result[propertyName] = object[propertyName];
69
+ return result;
70
+ }
71
+ JAVASCRIPT
72
+ object_handle = evaluate_handle(js, name)
73
+ properties = object_handle.properties
74
+ result = properties[name]
75
+ object_handle.dispose
76
+ result
77
+ end
78
+
79
+ # @param name [String]
80
+ # @return [Puppeteer::JSHandle]
81
+ def [](name)
82
+ property(name)
83
+ end
75
84
 
76
85
  # getProperties in JavaScript.
77
86
  # @return [Hash<String, JSHandle>]
@@ -101,7 +110,7 @@ class Puppeteer::JSHandle
101
110
  #
102
111
  # However it would be better that RemoteObject is responsible for
103
112
  # the logic `if (this._remoteObject.objectId) { ... }`.
104
- @remote_object.evaluate_self(@client) || @remote_object.value
113
+ @remote_object.evaluate_self(@client)&.value || @remote_object.value
105
114
  end
106
115
 
107
116
  def as_element
@@ -119,15 +128,16 @@ class Puppeteer::JSHandle
119
128
  @disposed
120
129
  end
121
130
 
122
- # /**
123
- # * @override
124
- # * @return {string}
125
- # */
126
- # toString() {
127
- # if (this._remoteObject.objectId) {
128
- # const type = this._remoteObject.subtype || this._remoteObject.type;
129
- # return 'JSHandle@' + type;
130
- # }
131
- # return 'JSHandle:' + helper.valueFromRemoteObject(this._remoteObject);
132
- # }
131
+ def to_s
132
+ # original logic was:
133
+ # if (this._remoteObject.objectId) {
134
+ # const type = this._remoteObject.subtype || this._remoteObject.type;
135
+ # return 'JSHandle@' + type;
136
+ # }
137
+ # return 'JSHandle:' + helper.valueFromRemoteObject(this._remoteObject);
138
+ #
139
+ # However it would be better that RemoteObject is responsible for
140
+ # the logic `if (this._remoteObject.objectId) { ... }`.
141
+ if_present(@remote_object.type_str) { |type_str| "JSHandle@#{type_str}" } || "JSHandle:#{@remote_object.value || 'undefined'}"
142
+ end
133
143
  end
@@ -89,7 +89,7 @@ class Puppeteer::Keyboard
89
89
  'Digit6': KeyDefinition.new({ 'keyCode': 54, 'code': 'Digit6', 'shiftKey': '^', 'key': '6' }),
90
90
  'Digit7': KeyDefinition.new({ 'keyCode': 55, 'code': 'Digit7', 'shiftKey': '&', 'key': '7' }),
91
91
  'Digit8': KeyDefinition.new({ 'keyCode': 56, 'code': 'Digit8', 'shiftKey': '*', 'key': '8' }),
92
- 'Digit9': KeyDefinition.new({ 'keyCode': 57, 'code': 'Digit9', 'shiftKey': '\(', 'key': '9' }),
92
+ 'Digit9': KeyDefinition.new({ 'keyCode': 57, 'code': 'Digit9', 'shiftKey': '(', 'key': '9' }),
93
93
  'KeyA': KeyDefinition.new({ 'keyCode': 65, 'code': 'KeyA', 'shiftKey': 'A', 'key': 'a' }),
94
94
  'KeyB': KeyDefinition.new({ 'keyCode': 66, 'code': 'KeyB', 'shiftKey': 'B', 'key': 'b' }),
95
95
  'KeyC': KeyDefinition.new({ 'keyCode': 67, 'code': 'KeyC', 'shiftKey': 'C', 'key': 'c' }),
@@ -235,7 +235,7 @@ class Puppeteer::Keyboard
235
235
  '%': KeyDefinition.new({ 'keyCode': 53, 'key': '%', 'code': 'Digit5' }),
236
236
  '^': KeyDefinition.new({ 'keyCode': 54, 'key': '^', 'code': 'Digit6' }),
237
237
  '&': KeyDefinition.new({ 'keyCode': 55, 'key': '&', 'code': 'Digit7' }),
238
- '(': KeyDefinition.new({ 'keyCode': 57, 'key': '\(', 'code': 'Digit9' }),
238
+ '(': KeyDefinition.new({ 'keyCode': 57, 'key': '(', 'code': 'Digit9' }),
239
239
  'A': KeyDefinition.new({ 'keyCode': 65, 'key': 'A', 'code': 'KeyA' }),
240
240
  'B': KeyDefinition.new({ 'keyCode': 66, 'key': 'B', 'code': 'KeyB' }),
241
241
  'C': KeyDefinition.new({ 'keyCode': 67, 'key': 'C', 'code': 'KeyC' }),
@@ -2,6 +2,7 @@ require_relative './launcher/base'
2
2
  require_relative './launcher/browser_options'
3
3
  require_relative './launcher/chrome'
4
4
  require_relative './launcher/chrome_arg_options'
5
+ require_relative './launcher/firefox'
5
6
  require_relative './launcher/launch_options'
6
7
 
7
8
  # https://github.com/puppeteer/puppeteer/blob/main/src/node/Launcher.ts
@@ -9,11 +10,19 @@ module Puppeteer::Launcher
9
10
  # @param project_root [String]
10
11
  # @param prefereed_revision [String]
11
12
  # @param is_puppeteer_core [String]
12
- # @param product [String] 'chrome' or 'firefox' (not implemented yet)
13
+ # @param product [String] 'chrome' or 'firefox'
13
14
  # @return [Puppeteer::Launcher::Chrome]
14
15
  module_function def new(project_root:, preferred_revision:, is_puppeteer_core:, product:)
16
+ unless is_puppeteer_core
17
+ product ||= ENV['PUPPETEER_PRODUCT']
18
+ end
19
+
15
20
  if product == 'firefox'
16
- raise NotImplementedError.new('FirefoxLauncher is not implemented yet.')
21
+ return Firefox.new(
22
+ project_root: project_root,
23
+ preferred_revision: preferred_revision,
24
+ is_puppeteer_core: is_puppeteer_core,
25
+ )
17
26
  end
18
27
 
19
28
  Chrome.new(
@@ -20,15 +20,25 @@ module Puppeteer::Launcher
20
20
  return executable_path
21
21
  end
22
22
  raise ExecutablePathNotFound.new(
23
- "Tried to use PUPPETEER_EXECUTABLE_PATH env variable to launch browser but did not find any executable at: #{executablePath}",
23
+ "Tried to use PUPPETEER_EXECUTABLE_PATH env variable to launch browser but did not find any executable at: #{executable_path}",
24
24
  )
25
25
  end
26
26
 
27
27
  # temporal logic.
28
- if RUBY_PLATFORM.include?('darwin') # MacOS
29
- '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
28
+ if Puppeteer.env.darwin?
29
+ case self
30
+ when Chrome
31
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
32
+ when Firefox
33
+ '/Applications/Firefox Nightly.app/Contents/MacOS/firefox'
34
+ end
30
35
  else
31
- '/usr/bin/google-chrome'
36
+ case self
37
+ when Chrome
38
+ '/usr/bin/google-chrome'
39
+ when Firefox
40
+ '/usr/bin/firefox'
41
+ end
32
42
  end
33
43
 
34
44
  # const browserFetcher = new BrowserFetcher(launcher._projectRoot);
@@ -28,7 +28,8 @@ module Puppeteer::Launcher
28
28
  # @property {number=} slowMo
29
29
  def initialize(options)
30
30
  @ignore_https_errors = options[:ignore_https_errors] || false
31
- @default_viewport = options[:default_viewport] || Puppeteer::Viewport.new(width: 800, height: 600)
31
+ # `default_viewport: nil` must be respected here.
32
+ @default_viewport = options.key?(:default_viewport) ? options[:default_viewport] : Puppeteer::Viewport.new(width: 800, height: 600)
32
33
  @slow_mo = options[:slow_mo] || 0
33
34
  end
34
35
 
@@ -12,9 +12,9 @@ module Puppeteer::Launcher
12
12
 
13
13
  chrome_arguments =
14
14
  if !@launch_options.ignore_default_args
15
- default_args.to_a
15
+ default_args(options).to_a
16
16
  elsif @launch_options.ignore_default_args.is_a?(Enumerable)
17
- default_args.reject do |arg|
17
+ default_args(options).reject do |arg|
18
18
  @launch_options.ignore_default_args.include?(arg)
19
19
  end.to_a
20
20
  else
@@ -34,7 +34,7 @@ module Puppeteer::Launcher
34
34
 
35
35
  temporary_user_data_dir = nil
36
36
  if chrome_arguments.none? { |arg| arg.start_with?('--user-data-dir') }
37
- temporary_user_data_dir = Dir.mktmpdir('puppeteer_dev_profile-')
37
+ temporary_user_data_dir = Dir.mktmpdir('puppeteer_dev_chrome_profile-')
38
38
  chrome_arguments << "--user-data-dir=#{temporary_user_data_dir}"
39
39
  end
40
40
 
@@ -141,11 +141,7 @@ module Puppeteer::Launcher
141
141
 
142
142
  # @return [DefaultArgs]
143
143
  def default_args(options = nil)
144
- if options.nil?
145
- @default_args ||= DefaultArgs.new(@chrome_arg_options)
146
- else
147
- DefaultArgs.new(ChromeArgOptions.new(options))
148
- end
144
+ DefaultArgs.new(ChromeArgOptions.new(options || {}))
149
145
  end
150
146
 
151
147
  # @return [Puppeteer::Browser]
@@ -206,7 +202,7 @@ module Puppeteer::Launcher
206
202
  resolve_executable_path
207
203
  end
208
204
 
209
- private def product
205
+ def product
210
206
  'chrome'
211
207
  end
212
208
  end
@@ -0,0 +1,385 @@
1
+ require 'tmpdir'
2
+
3
+ # https://github.com/puppeteer/puppeteer/blob/main/src/node/Launcher.ts
4
+ module Puppeteer::Launcher
5
+ class Firefox < Base
6
+ # @param {!(Launcher.LaunchOptions & Launcher.ChromeArgOptions & Launcher.BrowserOptions)=} options
7
+ # @return {!Promise<!Browser>}
8
+ def launch(options = {})
9
+ @chrome_arg_options = ChromeArgOptions.new(options)
10
+ @launch_options = LaunchOptions.new(options)
11
+ @browser_options = BrowserOptions.new(options)
12
+
13
+ firefox_arguments =
14
+ if !@launch_options.ignore_default_args
15
+ default_args(options).to_a
16
+ elsif @launch_options.ignore_default_args.is_a?(Enumerable)
17
+ default_args(options).reject do |arg|
18
+ @launch_options.ignore_default_args.include?(arg)
19
+ end.to_a
20
+ else
21
+ @chrome_arg_options.args.dup
22
+ end
23
+
24
+ if firefox_arguments.none? { |arg| arg.start_with?('--remote-debugging-') }
25
+ firefox_arguments << '--remote-debugging-port=0'
26
+ end
27
+
28
+ temporary_user_data_dir = nil
29
+ if firefox_arguments.none? { |arg| arg.start_with?('--profile') || arg.start_with?('-profile') }
30
+ temporary_user_data_dir = create_profile
31
+ firefox_arguments << "--profile"
32
+ firefox_arguments << temporary_user_data_dir
33
+ end
34
+
35
+ firefox_executable = @launch_options.executable_path || resolve_executable_path
36
+ runner = Puppeteer::BrowserRunner.new(firefox_executable, firefox_arguments, temporary_user_data_dir)
37
+ runner.start(
38
+ handle_SIGHUP: @launch_options.handle_SIGHUP?,
39
+ handle_SIGTERM: @launch_options.handle_SIGTERM?,
40
+ handle_SIGINT: @launch_options.handle_SIGINT?,
41
+ dumpio: @launch_options.dumpio?,
42
+ env: @launch_options.env,
43
+ pipe: @launch_options.pipe?,
44
+ )
45
+
46
+ begin
47
+ connection = runner.setup_connection(
48
+ use_pipe: @launch_options.pipe?,
49
+ timeout: @launch_options.timeout,
50
+ slow_mo: @browser_options.slow_mo,
51
+ preferred_revision: @preferred_revision,
52
+ )
53
+
54
+ browser = Puppeteer::Browser.create(
55
+ connection: connection,
56
+ context_ids: [],
57
+ ignore_https_errors: @browser_options.ignore_https_errors?,
58
+ default_viewport: @browser_options.default_viewport,
59
+ process: runner.proc,
60
+ close_callback: -> { runner.close },
61
+ )
62
+
63
+ browser.wait_for_target(predicate: ->(target) { target.type == 'page' })
64
+
65
+ browser
66
+ rescue
67
+ runner.kill
68
+ raise
69
+ end
70
+ end
71
+
72
+ # @return [Puppeteer::Browser]
73
+ def connect(options = {})
74
+ @browser_options = BrowserOptions.new(options)
75
+ browser_ws_endpoint = options[:browser_ws_endpoint]
76
+ browser_url = options[:browser_url]
77
+ transport = options[:transport]
78
+
79
+ connection =
80
+ if browser_ws_endpoint && browser_url.nil? && transport.nil?
81
+ connect_with_browser_ws_endpoint(browser_ws_endpoint)
82
+ elsif browser_ws_endpoint.nil? && browser_url && transport.nil?
83
+ connect_with_browser_url(browser_url)
84
+ elsif browser_ws_endpoint.nil? && browser_url.nil? && transport
85
+ connect_with_transport(transport)
86
+ else
87
+ raise ArgumentError.new("Exactly one of browserWSEndpoint, browserURL or transport must be passed to puppeteer.connect")
88
+ end
89
+
90
+ result = connection.send_message('Target.getBrowserContexts')
91
+ browser_context_ids = result['browserContextIds']
92
+
93
+ Puppeteer::Browser.create(
94
+ connection: connection,
95
+ context_ids: browser_context_ids,
96
+ ignore_https_errors: @browser_options.ignore_https_errors?,
97
+ default_viewport: @browser_options.default_viewport,
98
+ process: nil,
99
+ close_callback: -> { connection.send_message('Browser.close') },
100
+ )
101
+ end
102
+
103
+ # @return [Puppeteer::Connection]
104
+ private def connect_with_browser_ws_endpoint(browser_ws_endpoint)
105
+ transport = Puppeteer::WebSocketTransport.create(browser_ws_endpoint)
106
+ Puppeteer::Connection.new(browser_ws_endpoint, transport, @browser_options.slow_mo)
107
+ end
108
+
109
+ # @return [Puppeteer::Connection]
110
+ private def connect_with_browser_url(browser_url)
111
+ require 'net/http'
112
+ uri = URI(browser_url)
113
+ uri.path = '/json/version'
114
+ response_body = Net::HTTP.get(uri)
115
+ json = JSON.parse(response_body)
116
+ connection_url = json['webSocketDebuggerUrl']
117
+ connect_with_browser_ws_endpoint(connection_url)
118
+ end
119
+
120
+ # @return [Puppeteer::Connection]
121
+ private def connect_with_transport(transport)
122
+ Puppeteer::Connection.new('', transport, @browser_options.slow_mo)
123
+ end
124
+
125
+ # @return {string}
126
+ def executable_path
127
+ resolve_executable_path
128
+ end
129
+
130
+ def product
131
+ 'firefox'
132
+ end
133
+
134
+ class DefaultArgs
135
+ include Enumerable
136
+
137
+ # @param options [Launcher::ChromeArgOptions]
138
+ def initialize(chrome_arg_options)
139
+ firefox_arguments = ['--no-remote', '--foreground']
140
+
141
+ # if (os.platform().startsWith('win')) {
142
+ # firefoxArguments.push('--wait-for-browser');
143
+ # }
144
+
145
+ if chrome_arg_options.user_data_dir
146
+ firefox_arguments << "--profile"
147
+ firefox_arguments << chrome_arg_options.user_data_dir
148
+ end
149
+
150
+ if chrome_arg_options.headless?
151
+ firefox_arguments << '--headless'
152
+ end
153
+
154
+ if chrome_arg_options.devtools?
155
+ firefox_arguments << '--devtools'
156
+ end
157
+
158
+ if chrome_arg_options.args.all? { |arg| arg.start_with?('-') }
159
+ firefox_arguments << 'about:blank'
160
+ end
161
+
162
+ firefox_arguments.concat(chrome_arg_options.args)
163
+
164
+ @firefox_arguments = firefox_arguments
165
+ end
166
+
167
+ def each(&block)
168
+ @firefox_arguments.each do |opt|
169
+ block.call(opt)
170
+ end
171
+ end
172
+ end
173
+
174
+ # @return [DefaultArgs]
175
+ def default_args(options = nil)
176
+ DefaultArgs.new(ChromeArgOptions.new(options || {}))
177
+ end
178
+
179
+ private def create_profile(extra_prefs = {})
180
+ Dir.mktmpdir('puppeteer_dev_firefox_profile-').tap do |profile_path|
181
+ server = 'dummy.test'
182
+ default_preferences = {
183
+ # Make sure Shield doesn't hit the network.
184
+ 'app.normandy.api_url': '',
185
+ # Disable Firefox old build background check
186
+ 'app.update.checkInstallTime': false,
187
+ # Disable automatically upgrading Firefox
188
+ 'app.update.disabledForTesting': true,
189
+
190
+ # Increase the APZ content response timeout to 1 minute
191
+ 'apz.content_response_timeout': 60000,
192
+
193
+ # Prevent various error message on the console
194
+ # jest-puppeteer asserts that no error message is emitted by the console
195
+ 'browser.contentblocking.features.standard': '-tp,tpPrivate,cookieBehavior0,-cm,-fp',
196
+
197
+ # Enable the dump function: which sends messages to the system
198
+ # console
199
+ # https://bugzilla.mozilla.org/show_bug.cgi?id=1543115
200
+ 'browser.dom.window.dump.enabled': true,
201
+ # Disable topstories
202
+ 'browser.newtabpage.activity-stream.feeds.system.topstories': false,
203
+ # Always display a blank page
204
+ 'browser.newtabpage.enabled': false,
205
+ # Background thumbnails in particular cause grief: and disabling
206
+ # thumbnails in general cannot hurt
207
+ 'browser.pagethumbnails.capturing_disabled': true,
208
+
209
+ # Disable safebrowsing components.
210
+ 'browser.safebrowsing.blockedURIs.enabled': false,
211
+ 'browser.safebrowsing.downloads.enabled': false,
212
+ 'browser.safebrowsing.malware.enabled': false,
213
+ 'browser.safebrowsing.passwords.enabled': false,
214
+ 'browser.safebrowsing.phishing.enabled': false,
215
+
216
+ # Disable updates to search engines.
217
+ 'browser.search.update': false,
218
+ # Do not restore the last open set of tabs if the browser has crashed
219
+ 'browser.sessionstore.resume_from_crash': false,
220
+ # Skip check for default browser on startup
221
+ 'browser.shell.checkDefaultBrowser': false,
222
+
223
+ # Disable newtabpage
224
+ 'browser.startup.homepage': 'about:blank',
225
+ # Do not redirect user when a milstone upgrade of Firefox is detected
226
+ 'browser.startup.homepage_override.mstone': 'ignore',
227
+ # Start with a blank page about:blank
228
+ 'browser.startup.page': 0,
229
+
230
+ # Do not allow background tabs to be zombified on Android: otherwise for
231
+ # tests that open additional tabs: the test harness tab itself might get
232
+ # unloaded
233
+ 'browser.tabs.disableBackgroundZombification': false,
234
+ # Do not warn when closing all other open tabs
235
+ 'browser.tabs.warnOnCloseOtherTabs': false,
236
+ # Do not warn when multiple tabs will be opened
237
+ 'browser.tabs.warnOnOpen': false,
238
+
239
+ # Disable the UI tour.
240
+ 'browser.uitour.enabled': false,
241
+ # Turn off search suggestions in the location bar so as not to trigger
242
+ # network connections.
243
+ 'browser.urlbar.suggest.searches': false,
244
+ # Disable first run splash page on Windows 10
245
+ 'browser.usedOnWindows10.introURL': '',
246
+ # Do not warn on quitting Firefox
247
+ 'browser.warnOnQuit': false,
248
+
249
+ # Defensively disable data reporting systems
250
+ 'datareporting.healthreport.documentServerURI': "http://#{server}/dummy/healthreport/",
251
+ 'datareporting.healthreport.logging.consoleEnabled': false,
252
+ 'datareporting.healthreport.service.enabled': false,
253
+ 'datareporting.healthreport.service.firstRun': false,
254
+ 'datareporting.healthreport.uploadEnabled': false,
255
+
256
+ # Do not show datareporting policy notifications which can interfere with tests
257
+ 'datareporting.policy.dataSubmissionEnabled': false,
258
+ 'datareporting.policy.dataSubmissionPolicyBypassNotification': true,
259
+
260
+ # DevTools JSONViewer sometimes fails to load dependencies with its require.js.
261
+ # This doesn't affect Puppeteer but spams console (Bug 1424372)
262
+ 'devtools.jsonview.enabled': false,
263
+
264
+ # Disable popup-blocker
265
+ 'dom.disable_open_during_load': false,
266
+
267
+ # Enable the support for File object creation in the content process
268
+ # Required for |Page.setFileInputFiles| protocol method.
269
+ 'dom.file.createInChild': true,
270
+
271
+ # Disable the ProcessHangMonitor
272
+ 'dom.ipc.reportProcessHangs': false,
273
+
274
+ # Disable slow script dialogues
275
+ 'dom.max_chrome_script_run_time': 0,
276
+ 'dom.max_script_run_time': 0,
277
+
278
+ # Only load extensions from the application and user profile
279
+ # AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
280
+ 'extensions.autoDisableScopes': 0,
281
+ 'extensions.enabledScopes': 5,
282
+
283
+ # Disable metadata caching for installed add-ons by default
284
+ 'extensions.getAddons.cache.enabled': false,
285
+
286
+ # Disable installing any distribution extensions or add-ons.
287
+ 'extensions.installDistroAddons': false,
288
+
289
+ # Disabled screenshots extension
290
+ 'extensions.screenshots.disabled': true,
291
+
292
+ # Turn off extension updates so they do not bother tests
293
+ 'extensions.update.enabled': false,
294
+
295
+ # Turn off extension updates so they do not bother tests
296
+ 'extensions.update.notifyUser': false,
297
+
298
+ # Make sure opening about:addons will not hit the network
299
+ 'extensions.webservice.discoverURL': "http://#{server}/dummy/discoveryURL",
300
+
301
+ # Allow the application to have focus even it runs in the background
302
+ 'focusmanager.testmode': true,
303
+ # Disable useragent updates
304
+ 'general.useragent.updates.enabled': false,
305
+ # Always use network provider for geolocation tests so we bypass the
306
+ # macOS dialog raised by the corelocation provider
307
+ 'geo.provider.testing': true,
308
+ # Do not scan Wifi
309
+ 'geo.wifi.scan': false,
310
+ # No hang monitor
311
+ 'hangmonitor.timeout': 0,
312
+ # Show chrome errors and warnings in the error console
313
+ 'javascript.options.showInConsole': true,
314
+
315
+ # Disable download and usage of OpenH264: and Widevine plugins
316
+ 'media.gmp-manager.updateEnabled': false,
317
+ # Prevent various error message on the console
318
+ # jest-puppeteer asserts that no error message is emitted by the console
319
+ 'network.cookie.cookieBehavior': 0,
320
+
321
+ # Do not prompt for temporary redirects
322
+ 'network.http.prompt-temp-redirect': false,
323
+
324
+ # Disable speculative connections so they are not reported as leaking
325
+ # when they are hanging around
326
+ 'network.http.speculative-parallel-limit': 0,
327
+
328
+ # Do not automatically switch between offline and online
329
+ 'network.manage-offline-status': false,
330
+
331
+ # Make sure SNTP requests do not hit the network
332
+ 'network.sntp.pools': server,
333
+
334
+ # Disable Flash.
335
+ 'plugin.state.flash': 0,
336
+
337
+ 'privacy.trackingprotection.enabled': false,
338
+
339
+ # Enable Remote Agent
340
+ # https://bugzilla.mozilla.org/show_bug.cgi?id=1544393
341
+ 'remote.enabled': true,
342
+
343
+ # Don't do network connections for mitm priming
344
+ 'security.certerrors.mitm.priming.enabled': false,
345
+ # Local documents have access to all other local documents,
346
+ # including directory listings
347
+ 'security.fileuri.strict_origin_policy': false,
348
+ # Do not wait for the notification button security delay
349
+ 'security.notification_enable_delay': 0,
350
+
351
+ # Ensure blocklist updates do not hit the network
352
+ 'services.settings.server': "http://#{server}/dummy/blocklist/",
353
+
354
+ # Do not automatically fill sign-in forms with known usernames and
355
+ # passwords
356
+ 'signon.autofillForms': false,
357
+ # Disable password capture, so that tests that include forms are not
358
+ # influenced by the presence of the persistent doorhanger notification
359
+ 'signon.rememberSignons': false,
360
+
361
+ # Disable first-run welcome page
362
+ 'startup.homepage_welcome_url': 'about:blank',
363
+
364
+ # Disable first-run welcome page
365
+ 'startup.homepage_welcome_url.additional': '',
366
+
367
+ # Disable browser animations (tabs, fullscreen, sliding alerts)
368
+ 'toolkit.cosmeticAnimations.enabled': false,
369
+
370
+ # Prevent starting into safe mode after application crashes
371
+ 'toolkit.startup.max_resumed_crashes': -1,
372
+ }
373
+
374
+ preferences = default_preferences.merge(extra_prefs)
375
+
376
+ File.open(File.join(profile_path, 'user.js'), 'w') do |f|
377
+ preferences.each do |key, value|
378
+ f.write("user_pref(#{JSON.generate(key)}, #{JSON.generate(value)});\n")
379
+ end
380
+ end
381
+ IO.write(File.join(profile_path, 'prefs.js'), "")
382
+ end
383
+ end
384
+ end
385
+ end