puppeteer-ruby 0.0.17 → 0.0.22

Sign up to get free protection for your applications and to get access to all the features.
@@ -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