ferrum 0.12 → 0.13

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.
@@ -6,16 +6,14 @@ require "ferrum/page"
6
6
  require "ferrum/proxy"
7
7
  require "ferrum/contexts"
8
8
  require "ferrum/browser/xvfb"
9
+ require "ferrum/browser/options"
9
10
  require "ferrum/browser/process"
10
11
  require "ferrum/browser/client"
11
12
  require "ferrum/browser/binary"
13
+ require "ferrum/browser/version_info"
12
14
 
13
15
  module Ferrum
14
16
  class Browser
15
- DEFAULT_TIMEOUT = ENV.fetch("FERRUM_DEFAULT_TIMEOUT", 5).to_i
16
- WINDOW_SIZE = [1024, 768].freeze
17
- BASE_URL_SCHEMA = %w[http https].freeze
18
-
19
17
  extend Forwardable
20
18
  delegate %i[default_context] => :contexts
21
19
  delegate %i[targets create_target page pages windows] => :default_context
@@ -32,66 +30,146 @@ module Ferrum
32
30
  playback_rate playback_rate=] => :page
33
31
  delegate %i[default_user_agent] => :process
34
32
 
35
- attr_reader :client, :process, :contexts, :logger, :js_errors, :pending_connection_errors,
36
- :slowmo, :base_url, :options, :window_size, :ws_max_receive_size, :proxy_options,
37
- :proxy_server
38
- attr_writer :timeout
33
+ attr_reader :client, :process, :contexts, :options, :window_size, :base_url
34
+ attr_accessor :timeout
39
35
 
36
+ #
37
+ # Initializes the browser.
38
+ #
39
+ # @param [Hash{Symbol => Object}, nil] options
40
+ # Additional browser options.
41
+ #
42
+ # @option options [Boolean] :headless (true)
43
+ # Set browser as headless or not.
44
+ #
45
+ # @option options [Boolean] :xvfb (false)
46
+ # Run browser in a virtual framebuffer.
47
+ #
48
+ # @option options [(Integer, Integer)] :window_size ([1024, 768])
49
+ # The dimensions of the browser window in which to test, expressed as a
50
+ # 2-element array, e.g. `[1024, 768]`.
51
+ #
52
+ # @option options [Array<String, Hash>] :extensions
53
+ # An array of paths to files or JS source code to be preloaded into the
54
+ # browser e.g.: `["/path/to/script.js", { source: "window.secret = 'top'" }]`
55
+ #
56
+ # @option options [#puts] :logger
57
+ # When present, debug output is written to this object.
58
+ #
59
+ # @option options [Integer, Float] :slowmo
60
+ # Set a delay in seconds to wait before sending command.
61
+ # Useful companion of headless option, so that you have time to see
62
+ # changes.
63
+ #
64
+ # @option options [Numeric] :timeout (5)
65
+ # The number of seconds we'll wait for a response when communicating with
66
+ # browser.
67
+ #
68
+ # @option options [Boolean] :js_errors
69
+ # When true, JavaScript errors get re-raised in Ruby.
70
+ #
71
+ # @option options [Boolean] :pending_connection_errors (true)
72
+ # When main frame is still waiting for slow responses while timeout is
73
+ # reached {PendingConnectionsError} is raised. It's better to figure out
74
+ # why you have slow responses and fix or block them rather than turn this
75
+ # setting off.
76
+ #
77
+ # @option options [:chrome, :firefox] :browser_name (:chrome)
78
+ # Sets the browser's name. **Note:** only experimental support for
79
+ # `:firefox` for now.
80
+ #
81
+ # @option options [String] :browser_path
82
+ # Path to Chrome binary, you can also set ENV variable as
83
+ # `BROWSER_PATH=some/path/chrome bundle exec rspec`.
84
+ #
85
+ # @option options [Hash] :browser_options
86
+ # Additional command line options, [see them all](https://peter.sh/experiments/chromium-command-line-switches/)
87
+ # e.g. `{ "ignore-certificate-errors" => nil }`
88
+ #
89
+ # @option options [Boolean] :ignore_default_browser_options
90
+ # Ferrum has a number of default options it passes to the browser,
91
+ # if you set this to `true` then only options you put in
92
+ # `:browser_options` will be passed to the browser, except required ones
93
+ # of course.
94
+ #
95
+ # @option options [Integer] :port
96
+ # Remote debugging port for headless Chrome.
97
+ #
98
+ # @option options [String] :host
99
+ # Remote debugging address for headless Chrome.
100
+ #
101
+ # @option options [String] :url
102
+ # URL for a running instance of Chrome. If this is set, a browser process
103
+ # will not be spawned.
104
+ #
105
+ # @option options [Integer] :process_timeout
106
+ # How long to wait for the Chrome process to respond on startup.
107
+ #
108
+ # @option options [Integer] :ws_max_receive_size
109
+ # How big messages to accept from Chrome over the web socket, in bytes.
110
+ # Defaults to 64MB. Incoming messages larger this will cause a
111
+ # {Ferrum::DeadBrowserError}.
112
+ #
113
+ # @option options [Hash] :proxy
114
+ # Specify proxy settings, [read more](https://github.com/rubycdp/ferrum#proxy).
115
+ #
116
+ # @option options [String] :save_path
117
+ # Path to save attachments with [Content-Disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition)
118
+ # header.
119
+ #
120
+ # @option options [Hash] :env
121
+ # Environment variables you'd like to pass through to the process.
122
+ #
40
123
  def initialize(options = nil)
41
- options ||= {}
42
-
43
- @client = nil
44
- @window_size = options.fetch(:window_size, WINDOW_SIZE)
45
- @original_window_size = @window_size
46
-
47
- @options = Hash(options.merge(window_size: @window_size))
48
- @logger, @timeout, @ws_max_receive_size =
49
- @options.values_at(:logger, :timeout, :ws_max_receive_size)
50
- @js_errors = @options.fetch(:js_errors, false)
51
-
52
- if @options[:proxy]
53
- @proxy_options = @options[:proxy]
54
-
55
- if @proxy_options[:server]
56
- @proxy_server = Proxy.start(**@proxy_options.slice(:host, :port, :user, :password))
57
- @proxy_options.merge!(host: @proxy_server.host, port: @proxy_server.port)
58
- end
59
-
60
- @options[:browser_options] ||= {}
61
- address = "#{@proxy_options[:host]}:#{@proxy_options[:port]}"
62
- @options[:browser_options].merge!("proxy-server" => address)
63
- @options[:browser_options].merge!("proxy-bypass-list" => @proxy_options[:bypass]) if @proxy_options[:bypass]
64
- end
65
-
66
- @pending_connection_errors = @options.fetch(:pending_connection_errors, true)
67
- @slowmo = @options[:slowmo].to_f
68
-
69
- self.base_url = @options[:base_url] if @options.key?(:base_url)
70
-
71
- if ENV.fetch("FERRUM_DEBUG", nil) && !@logger
72
- $stdout.sync = true
73
- @logger = $stdout
74
- @options[:logger] = @logger
75
- end
124
+ @options = Options.new(options)
125
+ @client = @process = @contexts = nil
76
126
 
77
- @options.freeze
127
+ @timeout = @options.timeout
128
+ @window_size = @options.window_size
129
+ @base_url = @options.base_url if @options.base_url
78
130
 
79
131
  start
80
132
  end
81
133
 
134
+ #
135
+ # Sets the base URL.
136
+ #
137
+ # @param [String] value
138
+ # The new base URL value.
139
+ #
140
+ # @raise [ArgumentError] when path is not absolute or doesn't include schema
141
+ #
142
+ # @return [Addressable::URI]
143
+ # The parsed base URI value.
144
+ #
82
145
  def base_url=(value)
83
- parsed = Addressable::URI.parse(value)
84
- unless BASE_URL_SCHEMA.include?(parsed.normalized_scheme)
85
- raise "Set `base_url` should be absolute and include schema: #{BASE_URL_SCHEMA}"
86
- end
87
-
88
- @base_url = parsed
146
+ @base_url = options.parse_base_url(value)
89
147
  end
90
148
 
91
- def create_page(new_context: false)
92
- page = if new_context
93
- context = contexts.create
94
- context.create_page
149
+ #
150
+ # Creates a new page.
151
+ #
152
+ # @param [Boolean] new_context
153
+ # Whether to create a page in a new context or not.
154
+ #
155
+ # @param [Hash] proxy
156
+ # Whether to use proxy for a page. The page will be created in a new context if so.
157
+ #
158
+ # @return [Ferrum::Page]
159
+ # Created page.
160
+ #
161
+ def create_page(new_context: false, proxy: nil)
162
+ page = if new_context || proxy
163
+ params = {}
164
+
165
+ if proxy
166
+ options.parse_proxy(proxy)
167
+ params.merge!(proxyServer: "#{proxy[:host]}:#{proxy[:port]}")
168
+ params.merge!(proxyBypassList: proxy[:bypass]) if proxy[:bypass]
169
+ end
170
+
171
+ context = contexts.create(**params)
172
+ context.create_page(proxy: proxy)
95
173
  else
96
174
  default_context.create_page
97
175
  end
@@ -105,19 +183,28 @@ module Ferrum
105
183
  end
106
184
 
107
185
  def extensions
108
- @extensions ||= Array(@options[:extensions]).map do |ext|
186
+ @extensions ||= Array(options.extensions).map do |ext|
109
187
  (ext.is_a?(Hash) && ext[:source]) || File.read(ext)
110
188
  end
111
189
  end
112
190
 
191
+ #
192
+ # Evaluate JavaScript to modify things before a page load.
193
+ #
194
+ # @param [String] expression
195
+ # The JavaScript to add to each new document.
196
+ #
197
+ # @example
198
+ # browser.evaluate_on_new_document <<~JS
199
+ # Object.defineProperty(navigator, "languages", {
200
+ # get: function() { return ["tlh"]; }
201
+ # });
202
+ # JS
203
+ #
113
204
  def evaluate_on_new_document(expression)
114
205
  extensions << expression
115
206
  end
116
207
 
117
- def timeout
118
- @timeout || DEFAULT_TIMEOUT
119
- end
120
-
121
208
  def command(*args)
122
209
  @client.command(*args)
123
210
  rescue DeadBrowserError
@@ -125,8 +212,22 @@ module Ferrum
125
212
  raise
126
213
  end
127
214
 
215
+ #
216
+ # Closes browser tabs opened by the `Browser` instance.
217
+ #
218
+ # @example
219
+ # # connect to a long-running Chrome process
220
+ # browser = Ferrum::Browser.new(url: 'http://localhost:9222')
221
+ #
222
+ # browser.go_to("https://github.com/")
223
+ #
224
+ # # clean up, lest the tab stays there hanging forever
225
+ # browser.reset
226
+ #
227
+ # browser.quit
228
+ #
128
229
  def reset
129
- @window_size = @original_window_size
230
+ @window_size = options.window_size
130
231
  contexts.reset
131
232
  end
132
233
 
@@ -150,12 +251,25 @@ module Ferrum
150
251
  command("Browser.crash")
151
252
  end
152
253
 
254
+ #
255
+ # Gets the version information from the browser.
256
+ #
257
+ # @return [VersionInfo]
258
+ #
259
+ # @since 0.13
260
+ #
261
+ def version
262
+ VersionInfo.new(command("Browser.getVersion"))
263
+ end
264
+
153
265
  private
154
266
 
155
267
  def start
156
268
  Utils::ElapsedTime.start
157
- @process = Process.start(@options)
158
- @client = Client.new(self, @process.ws_url)
269
+ @process = Process.start(options)
270
+ @client = Client.new(@process.ws_url, self,
271
+ logger: options.logger,
272
+ ws_max_receive_size: options.ws_max_receive_size)
159
273
  @contexts = Contexts.new(self)
160
274
  end
161
275
  end
@@ -40,8 +40,9 @@ module Ferrum
40
40
  windows.map(&:page)
41
41
  end
42
42
 
43
- def create_page
44
- create_target.page
43
+ def create_page(**options)
44
+ target = create_target
45
+ target.page = target.build_page(**options)
45
46
  end
46
47
 
47
48
  def create_target
@@ -23,8 +23,8 @@ module Ferrum
23
23
  context
24
24
  end
25
25
 
26
- def create
27
- response = @browser.command("Target.createBrowserContext")
26
+ def create(**options)
27
+ response = @browser.command("Target.createBrowserContext", **options)
28
28
  context_id = response["browserContextId"]
29
29
  context = Context.new(@browser, self, context_id)
30
30
  @contexts[context_id] = context
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ferrum
4
+ class Cookies
5
+ #
6
+ # Represents a [cookie value](https://chromedevtools.github.io/devtools-protocol/1-3/Network/#type-Cookie).
7
+ #
8
+ class Cookie
9
+ # The parsed JSON attributes.
10
+ #
11
+ # @return [Hash{String => [String, Boolean, nil]}]
12
+ attr_reader :attributes
13
+
14
+ #
15
+ # Initializes the cookie.
16
+ #
17
+ # @param [Hash{String => String}] attributes
18
+ # The parsed JSON attributes.
19
+ #
20
+ def initialize(attributes)
21
+ @attributes = attributes
22
+ end
23
+
24
+ #
25
+ # The cookie's name.
26
+ #
27
+ # @return [String]
28
+ #
29
+ def name
30
+ attributes["name"]
31
+ end
32
+
33
+ #
34
+ # The cookie's value.
35
+ #
36
+ # @return [String]
37
+ #
38
+ def value
39
+ attributes["value"]
40
+ end
41
+
42
+ #
43
+ # The cookie's domain.
44
+ #
45
+ # @return [String]
46
+ #
47
+ def domain
48
+ attributes["domain"]
49
+ end
50
+
51
+ #
52
+ # The cookie's path.
53
+ #
54
+ # @return [String]
55
+ #
56
+ def path
57
+ attributes["path"]
58
+ end
59
+
60
+ #
61
+ # The `sameSite` configuration.
62
+ #
63
+ # @return ["Strict", "Lax", "None", nil]
64
+ #
65
+ def samesite
66
+ attributes["sameSite"]
67
+ end
68
+ alias same_site samesite
69
+
70
+ #
71
+ # The cookie's size.
72
+ #
73
+ # @return [Integer]
74
+ #
75
+ def size
76
+ attributes["size"]
77
+ end
78
+
79
+ #
80
+ # Specifies whether the cookie is secure or not.
81
+ #
82
+ # @return [Boolean]
83
+ #
84
+ def secure?
85
+ attributes["secure"]
86
+ end
87
+
88
+ #
89
+ # Specifies whether the cookie is HTTP-only or not.
90
+ #
91
+ # @return [Boolean]
92
+ #
93
+ def httponly?
94
+ attributes["httpOnly"]
95
+ end
96
+ alias http_only? httponly?
97
+
98
+ #
99
+ # Specifies whether the cookie is a session cookie or not.
100
+ #
101
+ # @return [Boolean]
102
+ #
103
+ def session?
104
+ attributes["session"]
105
+ end
106
+
107
+ #
108
+ # Specifies when the cookie will expire.
109
+ #
110
+ # @return [Time, nil]
111
+ #
112
+ def expires
113
+ Time.at(attributes["expires"]) if attributes["expires"].positive?
114
+ end
115
+
116
+ #
117
+ # Compares different cookie objects.
118
+ #
119
+ # @return [Boolean]
120
+ #
121
+ def ==(other)
122
+ other.class == self.class && other.attributes == attributes
123
+ end
124
+ end
125
+ end
126
+ end
@@ -1,68 +1,83 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "ferrum/cookies/cookie"
4
+
3
5
  module Ferrum
4
6
  class Cookies
5
- class Cookie
6
- attr_reader :attributes
7
-
8
- def initialize(attributes)
9
- @attributes = attributes
10
- end
11
-
12
- def name
13
- @attributes["name"]
14
- end
15
-
16
- def value
17
- @attributes["value"]
18
- end
19
-
20
- def domain
21
- @attributes["domain"]
22
- end
23
-
24
- def path
25
- @attributes["path"]
26
- end
27
-
28
- def samesite
29
- @attributes["sameSite"]
30
- end
31
-
32
- def size
33
- @attributes["size"]
34
- end
35
-
36
- def secure?
37
- @attributes["secure"]
38
- end
39
-
40
- def httponly?
41
- @attributes["httpOnly"]
42
- end
43
-
44
- def session?
45
- @attributes["session"]
46
- end
47
-
48
- def expires
49
- Time.at(@attributes["expires"]) if @attributes["expires"].positive?
50
- end
51
- end
52
-
53
7
  def initialize(page)
54
8
  @page = page
55
9
  end
56
10
 
11
+ #
12
+ # Returns cookies hash.
13
+ #
14
+ # @return [Hash{String => Cookie}]
15
+ #
16
+ # @example
17
+ # browser.cookies.all # => {
18
+ # # "NID" => #<Ferrum::Cookies::Cookie:0x0000558624b37a40 @attributes={
19
+ # # "name"=>"NID", "value"=>"...", "domain"=>".google.com", "path"=>"/",
20
+ # # "expires"=>1583211046.575681, "size"=>178, "httpOnly"=>true, "secure"=>false, "session"=>false
21
+ # # }>
22
+ # # }
23
+ #
57
24
  def all
58
25
  cookies = @page.command("Network.getAllCookies")["cookies"]
59
26
  cookies.to_h { |c| [c["name"], Cookie.new(c)] }
60
27
  end
61
28
 
29
+ #
30
+ # Returns cookie.
31
+ #
32
+ # @param [String] name
33
+ # The cookie name to fetch.
34
+ #
35
+ # @return [Cookie, nil]
36
+ # The cookie with the matching name.
37
+ #
38
+ # @example
39
+ # browser.cookies["NID"] # =>
40
+ # # <Ferrum::Cookies::Cookie:0x0000558624b67a88 @attributes={
41
+ # # "name"=>"NID", "value"=>"...", "domain"=>".google.com",
42
+ # # "path"=>"/", "expires"=>1583211046.575681, "size"=>178,
43
+ # # "httpOnly"=>true, "secure"=>false, "session"=>false
44
+ # # }>
45
+ #
62
46
  def [](name)
63
47
  all[name]
64
48
  end
65
49
 
50
+ #
51
+ # Sets a cookie.
52
+ #
53
+ # @param [Hash{Symbol => Object}, Cookie] options
54
+ #
55
+ # @option options [String] :name
56
+ #
57
+ # @option options [String] :value
58
+ #
59
+ # @option options [String] :domain
60
+ #
61
+ # @option options [String] :path
62
+ #
63
+ # @option options [Integer] :expires
64
+ #
65
+ # @option options [Integer] :size
66
+ #
67
+ # @option options [Boolean] :httponly
68
+ #
69
+ # @option options [Boolean] :secure
70
+ #
71
+ # @option options [String] :samesite
72
+ #
73
+ #
74
+ # @example
75
+ # browser.cookies.set(name: "stealth", value: "omg", domain: "google.com") # => true
76
+ #
77
+ # @example
78
+ # nid_cookie = browser.cookies["NID"] # => <Ferrum::Cookies::Cookie:0x0000558624b67a88>
79
+ # browser.cookies.set(nid_cookie) # => true
80
+ #
66
81
  def set(options)
67
82
  cookie = (
68
83
  options.is_a?(Cookie) ? options.attributes : options
@@ -79,7 +94,21 @@ module Ferrum
79
94
  @page.command("Network.setCookie", **cookie)["success"]
80
95
  end
81
96
 
82
- # Supports :url, :domain and :path options
97
+ #
98
+ # Removes given cookie.
99
+ #
100
+ # @param [String] name
101
+ #
102
+ # @param [Hash{Symbol => Object}] options
103
+ # Additional keyword arguments.
104
+ #
105
+ # @option options [String] :domain
106
+ #
107
+ # @option options [String] :url
108
+ #
109
+ # @example
110
+ # browser.cookies.remove(name: "stealth", domain: "google.com") # => true
111
+ #
83
112
  def remove(name:, **options)
84
113
  raise "Specify :domain or :url option" if !options[:domain] && !options[:url] && !default_domain
85
114
 
@@ -91,6 +120,14 @@ module Ferrum
91
120
  true
92
121
  end
93
122
 
123
+ #
124
+ # Removes all cookies for current page.
125
+ #
126
+ # @return [true]
127
+ #
128
+ # @example
129
+ # browser.cookies.clear # => true
130
+ #
94
131
  def clear
95
132
  @page.command("Network.clearBrowserCookies")
96
133
  true
data/lib/ferrum/dialog.rb CHANGED
@@ -10,6 +10,22 @@ module Ferrum
10
10
  @default_prompt = params["defaultPrompt"]
11
11
  end
12
12
 
13
+ #
14
+ # Accept dialog with given text or default prompt if applicable
15
+ #
16
+ # @param [String, nil] prompt_text
17
+ #
18
+ # @example
19
+ # browser = Ferrum::Browser.new
20
+ # browser.on(:dialog) do |dialog|
21
+ # if dialog.match?(/bla-bla/)
22
+ # dialog.accept
23
+ # else
24
+ # dialog.dismiss
25
+ # end
26
+ # end
27
+ # browser.go_to("https://google.com")
28
+ #
13
29
  def accept(prompt_text = nil)
14
30
  options = { accept: true }
15
31
  response = prompt_text || default_prompt
@@ -17,6 +33,20 @@ module Ferrum
17
33
  @page.command("Page.handleJavaScriptDialog", slowmoable: true, **options)
18
34
  end
19
35
 
36
+ #
37
+ # Dismiss dialog.
38
+ #
39
+ # @example
40
+ # browser = Ferrum::Browser.new
41
+ # browser.on(:dialog) do |dialog|
42
+ # if dialog.match?(/bla-bla/)
43
+ # dialog.accept
44
+ # else
45
+ # dialog.dismiss
46
+ # end
47
+ # end
48
+ # browser.go_to("https://google.com")
49
+ #
20
50
  def dismiss
21
51
  @page.command("Page.handleJavaScriptDialog", slowmoable: true, accept: false)
22
52
  end