ferrum 0.12 → 0.14

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +28 -22
  3. data/lib/ferrum/browser/client.rb +6 -5
  4. data/lib/ferrum/browser/command.rb +9 -6
  5. data/lib/ferrum/browser/options/base.rb +1 -4
  6. data/lib/ferrum/browser/options/chrome.rb +22 -10
  7. data/lib/ferrum/browser/options/firefox.rb +3 -6
  8. data/lib/ferrum/browser/options.rb +84 -0
  9. data/lib/ferrum/browser/process.rb +6 -7
  10. data/lib/ferrum/browser/version_info.rb +71 -0
  11. data/lib/ferrum/browser/web_socket.rb +1 -1
  12. data/lib/ferrum/browser/xvfb.rb +1 -1
  13. data/lib/ferrum/browser.rb +184 -64
  14. data/lib/ferrum/context.rb +3 -2
  15. data/lib/ferrum/contexts.rb +2 -2
  16. data/lib/ferrum/cookies/cookie.rb +183 -0
  17. data/lib/ferrum/cookies.rb +122 -49
  18. data/lib/ferrum/dialog.rb +30 -0
  19. data/lib/ferrum/frame/dom.rb +177 -0
  20. data/lib/ferrum/frame/runtime.rb +41 -61
  21. data/lib/ferrum/frame.rb +91 -3
  22. data/lib/ferrum/headers.rb +28 -0
  23. data/lib/ferrum/keyboard.rb +45 -2
  24. data/lib/ferrum/mouse.rb +84 -0
  25. data/lib/ferrum/network/exchange.rb +104 -5
  26. data/lib/ferrum/network/intercepted_request.rb +3 -12
  27. data/lib/ferrum/network/request.rb +58 -19
  28. data/lib/ferrum/network/request_params.rb +57 -0
  29. data/lib/ferrum/network/response.rb +106 -4
  30. data/lib/ferrum/network.rb +193 -8
  31. data/lib/ferrum/node.rb +21 -1
  32. data/lib/ferrum/page/animation.rb +16 -0
  33. data/lib/ferrum/page/frames.rb +66 -11
  34. data/lib/ferrum/page/screenshot.rb +97 -0
  35. data/lib/ferrum/page/tracing.rb +26 -0
  36. data/lib/ferrum/page.rb +158 -45
  37. data/lib/ferrum/proxy.rb +91 -2
  38. data/lib/ferrum/target.rb +6 -4
  39. data/lib/ferrum/version.rb +1 -1
  40. metadata +7 -101
@@ -1,68 +1,119 @@
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
7
+ include Enumerable
35
8
 
36
- def secure?
37
- @attributes["secure"]
38
- end
9
+ def initialize(page)
10
+ @page = page
11
+ end
39
12
 
40
- def httponly?
41
- @attributes["httpOnly"]
42
- end
13
+ #
14
+ # Enumerates over all cookies.
15
+ #
16
+ # @yield [cookie]
17
+ # The given block will be passed each cookie.
18
+ #
19
+ # @yieldparam [Cookie] cookie
20
+ # A cookie in the browser.
21
+ #
22
+ # @return [Enumerator]
23
+ # If no block is given, an Enumerator object will be returned.
24
+ #
25
+ def each
26
+ return enum_for(__method__) unless block_given?
43
27
 
44
- def session?
45
- @attributes["session"]
46
- end
28
+ cookies = @page.command("Network.getAllCookies")["cookies"]
47
29
 
48
- def expires
49
- Time.at(@attributes["expires"]) if @attributes["expires"].positive?
30
+ cookies.each do |c|
31
+ yield Cookie.new(c)
50
32
  end
51
33
  end
52
34
 
53
- def initialize(page)
54
- @page = page
55
- end
56
-
35
+ #
36
+ # Returns cookies hash.
37
+ #
38
+ # @return [Hash{String => Cookie}]
39
+ #
40
+ # @example
41
+ # browser.cookies.all # => {
42
+ # # "NID" => #<Ferrum::Cookies::Cookie:0x0000558624b37a40 @attributes={
43
+ # # "name"=>"NID", "value"=>"...", "domain"=>".google.com", "path"=>"/",
44
+ # # "expires"=>1583211046.575681, "size"=>178, "httpOnly"=>true, "secure"=>false, "session"=>false
45
+ # # }>
46
+ # # }
47
+ #
57
48
  def all
58
- cookies = @page.command("Network.getAllCookies")["cookies"]
59
- cookies.to_h { |c| [c["name"], Cookie.new(c)] }
49
+ each.to_h do |cookie|
50
+ [cookie.name, cookie]
51
+ end
60
52
  end
61
53
 
54
+ #
55
+ # Returns cookie.
56
+ #
57
+ # @param [String] name
58
+ # The cookie name to fetch.
59
+ #
60
+ # @return [Cookie, nil]
61
+ # The cookie with the matching name.
62
+ #
63
+ # @example
64
+ # browser.cookies["NID"] # =>
65
+ # # <Ferrum::Cookies::Cookie:0x0000558624b67a88 @attributes={
66
+ # # "name"=>"NID", "value"=>"...", "domain"=>".google.com",
67
+ # # "path"=>"/", "expires"=>1583211046.575681, "size"=>178,
68
+ # # "httpOnly"=>true, "secure"=>false, "session"=>false
69
+ # # }>
70
+ #
62
71
  def [](name)
63
- all[name]
72
+ find { |cookie| cookie.name == name }
64
73
  end
65
74
 
75
+ #
76
+ # Sets a cookie.
77
+ #
78
+ # @param [Hash{Symbol => Object}, Cookie] options
79
+ #
80
+ # @option options [String] :name
81
+ # The cookie param name.
82
+ #
83
+ # @option options [String] :value
84
+ # The cookie param value.
85
+ #
86
+ # @option options [String] :domain
87
+ # The domain the cookie belongs to.
88
+ #
89
+ # @option options [String] :path
90
+ # The path that the cookie is bound to.
91
+ #
92
+ # @option options [Integer] :expires
93
+ # When the cookie will expire.
94
+ #
95
+ # @option options [Integer] :size
96
+ # The size of the cookie.
97
+ #
98
+ # @option options [Boolean] :httponly
99
+ # Specifies whether the cookie `HttpOnly`.
100
+ #
101
+ # @option options [Boolean] :secure
102
+ # Specifies whether the cookie is marked as `Secure`.
103
+ #
104
+ # @option options [String] :samesite
105
+ # Specifies whether the cookie is `SameSite`.
106
+ #
107
+ # @option options [Boolean] :session
108
+ # Specifies whether the cookie is a session cookie.
109
+ #
110
+ # @example
111
+ # browser.cookies.set(name: "stealth", value: "omg", domain: "google.com") # => true
112
+ #
113
+ # @example
114
+ # nid_cookie = browser.cookies["NID"] # => <Ferrum::Cookies::Cookie:0x0000558624b67a88>
115
+ # browser.cookies.set(nid_cookie) # => true
116
+ #
66
117
  def set(options)
67
118
  cookie = (
68
119
  options.is_a?(Cookie) ? options.attributes : options
@@ -79,7 +130,21 @@ module Ferrum
79
130
  @page.command("Network.setCookie", **cookie)["success"]
80
131
  end
81
132
 
82
- # Supports :url, :domain and :path options
133
+ #
134
+ # Removes given cookie.
135
+ #
136
+ # @param [String] name
137
+ #
138
+ # @param [Hash{Symbol => Object}] options
139
+ # Additional keyword arguments.
140
+ #
141
+ # @option options [String] :domain
142
+ #
143
+ # @option options [String] :url
144
+ #
145
+ # @example
146
+ # browser.cookies.remove(name: "stealth", domain: "google.com") # => true
147
+ #
83
148
  def remove(name:, **options)
84
149
  raise "Specify :domain or :url option" if !options[:domain] && !options[:url] && !default_domain
85
150
 
@@ -91,6 +156,14 @@ module Ferrum
91
156
  true
92
157
  end
93
158
 
159
+ #
160
+ # Removes all cookies for current page.
161
+ #
162
+ # @return [true]
163
+ #
164
+ # @example
165
+ # browser.cookies.clear # => true
166
+ #
94
167
  def clear
95
168
  @page.command("Network.clearBrowserCookies")
96
169
  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
@@ -20,10 +20,59 @@
20
20
  module Ferrum
21
21
  class Frame
22
22
  module DOM
23
+ SCRIPT_SRC_TAG = <<~JS
24
+ const script = document.createElement("script");
25
+ script.src = arguments[0];
26
+ script.type = arguments[1];
27
+ script.onload = arguments[2];
28
+ document.head.appendChild(script);
29
+ JS
30
+ SCRIPT_TEXT_TAG = <<~JS
31
+ const script = document.createElement("script");
32
+ script.text = arguments[0];
33
+ script.type = arguments[1];
34
+ document.head.appendChild(script);
35
+ arguments[2]();
36
+ JS
37
+ STYLE_TAG = <<~JS
38
+ const style = document.createElement("style");
39
+ style.type = "text/css";
40
+ style.appendChild(document.createTextNode(arguments[0]));
41
+ document.head.appendChild(style);
42
+ arguments[1]();
43
+ JS
44
+ LINK_TAG = <<~JS
45
+ const link = document.createElement("link");
46
+ link.rel = "stylesheet";
47
+ link.href = arguments[0];
48
+ link.onload = arguments[1];
49
+ document.head.appendChild(link);
50
+ JS
51
+
52
+ #
53
+ # Returns current top window `location href`.
54
+ #
55
+ # @return [String]
56
+ # The window's current URL.
57
+ #
58
+ # @example
59
+ # browser.go_to("https://google.com/")
60
+ # browser.current_url # => "https://www.google.com/"
61
+ #
23
62
  def current_url
24
63
  evaluate("window.top.location.href")
25
64
  end
26
65
 
66
+ #
67
+ # Returns current top window title.
68
+ #
69
+ # @return [String]
70
+ # The window's current title.
71
+ #
72
+ # @example
73
+ # browser.go_to("https://google.com/")
74
+ # browser.current_title # => "Google"
75
+ #
27
76
  def current_title
28
77
  evaluate("window.top.document.title")
29
78
  end
@@ -32,10 +81,36 @@ module Ferrum
32
81
  evaluate("document.doctype && new XMLSerializer().serializeToString(document.doctype)")
33
82
  end
34
83
 
84
+ #
85
+ # Returns current page's html.
86
+ #
87
+ # @return [String]
88
+ # The HTML source of the current page.
89
+ #
90
+ # @example
91
+ # browser.go_to("https://google.com/")
92
+ # browser.body # => '<html itemscope="" itemtype="http://schema.org/WebPage" lang="ru"><head>...
93
+ #
35
94
  def body
36
95
  evaluate("document.documentElement.outerHTML")
37
96
  end
38
97
 
98
+ #
99
+ # Finds nodes by using a XPath selector.
100
+ #
101
+ # @param [String] selector
102
+ # The XPath selector.
103
+ #
104
+ # @param [Node, nil] within
105
+ # The parent node to search within.
106
+ #
107
+ # @return [Array<Node>]
108
+ # The matching nodes.
109
+ #
110
+ # @example
111
+ # browser.go_to("https://github.com/")
112
+ # browser.xpath("//a[@aria-label='Issues you created']") # => [Node]
113
+ #
39
114
  def xpath(selector, within: nil)
40
115
  expr = <<~JS
41
116
  function(selector, within) {
@@ -54,6 +129,22 @@ module Ferrum
54
129
  evaluate_func(expr, selector, within)
55
130
  end
56
131
 
132
+ #
133
+ # Finds a node by using a XPath selector.
134
+ #
135
+ # @param [String] selector
136
+ # The XPath selector.
137
+ #
138
+ # @param [Node, nil] within
139
+ # The parent node to search within.
140
+ #
141
+ # @return [Node, nil]
142
+ # The matching node.
143
+ #
144
+ # @example
145
+ # browser.go_to("https://github.com/")
146
+ # browser.at_xpath("//a[@aria-label='Issues you created']") # => Node
147
+ #
57
148
  def at_xpath(selector, within: nil)
58
149
  expr = <<~JS
59
150
  function(selector, within) {
@@ -65,6 +156,22 @@ module Ferrum
65
156
  evaluate_func(expr, selector, within)
66
157
  end
67
158
 
159
+ #
160
+ # Finds nodes by using a CSS path selector.
161
+ #
162
+ # @param [String] selector
163
+ # The CSS path selector.
164
+ #
165
+ # @param [Node, nil] within
166
+ # The parent node to search within.
167
+ #
168
+ # @return [Array<Node>]
169
+ # The matching nodes.
170
+ #
171
+ # @example
172
+ # browser.go_to("https://github.com/")
173
+ # browser.css("a[aria-label='Issues you created']") # => [Node]
174
+ #
68
175
  def css(selector, within: nil)
69
176
  expr = <<~JS
70
177
  function(selector, within) {
@@ -76,6 +183,22 @@ module Ferrum
76
183
  evaluate_func(expr, selector, within)
77
184
  end
78
185
 
186
+ #
187
+ # Finds a node by using a CSS path selector.
188
+ #
189
+ # @param [String] selector
190
+ # The CSS path selector.
191
+ #
192
+ # @param [Node, nil] within
193
+ # The parent node to search within.
194
+ #
195
+ # @return [Node, nil]
196
+ # The matching node.
197
+ #
198
+ # @example
199
+ # browser.go_to("https://github.com/")
200
+ # browser.at_css("a[aria-label='Issues you created']") # => Node
201
+ #
79
202
  def at_css(selector, within: nil)
80
203
  expr = <<~JS
81
204
  function(selector, within) {
@@ -86,6 +209,60 @@ module Ferrum
86
209
 
87
210
  evaluate_func(expr, selector, within)
88
211
  end
212
+
213
+ #
214
+ # Adds a `<script>` tag to the document.
215
+ #
216
+ # @param [String, nil] url
217
+ #
218
+ # @param [String, nil] path
219
+ #
220
+ # @param [String, nil] content
221
+ #
222
+ # @param [String] type
223
+ #
224
+ # @example
225
+ # browser.add_script_tag(url: "http://example.com/stylesheet.css") # => true
226
+ #
227
+ def add_script_tag(url: nil, path: nil, content: nil, type: "text/javascript")
228
+ expr, *args = if url
229
+ [SCRIPT_SRC_TAG, url, type]
230
+ elsif path || content
231
+ if path
232
+ content = File.read(path)
233
+ content += "\n//# sourceURL=#{path}"
234
+ end
235
+ [SCRIPT_TEXT_TAG, content, type]
236
+ end
237
+
238
+ evaluate_async(expr, @page.timeout, *args)
239
+ end
240
+
241
+ #
242
+ # Adds a `<style>` tag to the document.
243
+ #
244
+ # @param [String, nil] url
245
+ #
246
+ # @param [String, nil] path
247
+ #
248
+ # @param [String, nil] content
249
+ #
250
+ # @example
251
+ # browser.add_style_tag(content: "h1 { font-size: 40px; }") # => true
252
+ #
253
+ def add_style_tag(url: nil, path: nil, content: nil)
254
+ expr, *args = if url
255
+ [LINK_TAG, url]
256
+ elsif path || content
257
+ if path
258
+ content = File.read(path)
259
+ content += "\n//# sourceURL=#{path}"
260
+ end
261
+ [STYLE_TAG, content]
262
+ end
263
+
264
+ evaluate_async(expr, @page.timeout, *args)
265
+ end
89
266
  end
90
267
  end
91
268
  end
@@ -16,40 +16,38 @@ module Ferrum
16
16
  INTERMITTENT_ATTEMPTS = ENV.fetch("FERRUM_INTERMITTENT_ATTEMPTS", 6).to_i
17
17
  INTERMITTENT_SLEEP = ENV.fetch("FERRUM_INTERMITTENT_SLEEP", 0.1).to_f
18
18
 
19
- SCRIPT_SRC_TAG = <<~JS
20
- const script = document.createElement("script");
21
- script.src = arguments[0];
22
- script.type = arguments[1];
23
- script.onload = arguments[2];
24
- document.head.appendChild(script);
25
- JS
26
- SCRIPT_TEXT_TAG = <<~JS
27
- const script = document.createElement("script");
28
- script.text = arguments[0];
29
- script.type = arguments[1];
30
- document.head.appendChild(script);
31
- arguments[2]();
32
- JS
33
- STYLE_TAG = <<~JS
34
- const style = document.createElement("style");
35
- style.type = "text/css";
36
- style.appendChild(document.createTextNode(arguments[0]));
37
- document.head.appendChild(style);
38
- arguments[1]();
39
- JS
40
- LINK_TAG = <<~JS
41
- const link = document.createElement("link");
42
- link.rel = "stylesheet";
43
- link.href = arguments[0];
44
- link.onload = arguments[1];
45
- document.head.appendChild(link);
46
- JS
47
-
19
+ #
20
+ # Evaluate and return result for given JS expression.
21
+ #
22
+ # @param [String] expression
23
+ # The JavaScript to evaluate.
24
+ #
25
+ # @param [Array] args
26
+ # Additional arguments to pass to the JavaScript code.
27
+ #
28
+ # @example
29
+ # browser.evaluate("[window.scrollX, window.scrollY]")
30
+ #
48
31
  def evaluate(expression, *args)
49
32
  expression = format("function() { return %s }", expression)
50
33
  call(expression: expression, arguments: args)
51
34
  end
52
35
 
36
+ #
37
+ # Evaluate asynchronous expression and return result.
38
+ #
39
+ # @param [String] expression
40
+ # The JavaScript to evaluate.
41
+ #
42
+ # @param [Integer] wait
43
+ # How long we should wait for Promise to resolve or reject.
44
+ #
45
+ # @param [Array] args
46
+ # Additional arguments to pass to the JavaScript code.
47
+ #
48
+ # @example
49
+ # browser.evaluate_async(%(arguments[0]({foo: "bar"})), 5) # => { "foo" => "bar" }
50
+ #
53
51
  def evaluate_async(expression, wait, *args)
54
52
  template = <<~JS
55
53
  function() {
@@ -70,6 +68,18 @@ module Ferrum
70
68
  call(expression: expression, arguments: args, awaitPromise: true)
71
69
  end
72
70
 
71
+ #
72
+ # Execute expression. Doesn't return the result.
73
+ #
74
+ # @param [String] expression
75
+ # The JavaScript to evaluate.
76
+ #
77
+ # @param [Array] args
78
+ # Additional arguments to pass to the JavaScript code.
79
+ #
80
+ # @example
81
+ # browser.execute(%(1 + 1)) # => true
82
+ #
73
83
  def execute(expression, *args)
74
84
  expression = format("function() { %s }", expression)
75
85
  call(expression: expression, arguments: args, handle: false, returnByValue: true)
@@ -87,42 +97,12 @@ module Ferrum
87
97
  call(expression: expression, on: node, wait: wait, **options)
88
98
  end
89
99
 
90
- def add_script_tag(url: nil, path: nil, content: nil, type: "text/javascript")
91
- expr, *args = if url
92
- [SCRIPT_SRC_TAG, url, type]
93
- elsif path || content
94
- if path
95
- content = File.read(path)
96
- content += "\n//# sourceURL=#{path}"
97
- end
98
- [SCRIPT_TEXT_TAG, content, type]
99
- end
100
-
101
- evaluate_async(expr, @page.timeout, *args)
102
- end
103
-
104
- def add_style_tag(url: nil, path: nil, content: nil)
105
- expr, *args = if url
106
- [LINK_TAG, url]
107
- elsif path || content
108
- if path
109
- content = File.read(path)
110
- content += "\n//# sourceURL=#{path}"
111
- end
112
- [STYLE_TAG, content]
113
- end
114
-
115
- evaluate_async(expr, @page.timeout, *args)
116
- end
117
-
118
100
  private
119
101
 
120
102
  def call(expression:, arguments: [], on: nil, wait: 0, handle: true, **options)
121
103
  errors = [NodeNotFoundError, NoExecutionContextError]
122
- sleep = INTERMITTENT_SLEEP
123
- attempts = INTERMITTENT_ATTEMPTS
124
104
 
125
- Utils::Attempt.with_retry(errors: errors, max: attempts, wait: sleep) do
105
+ Utils::Attempt.with_retry(errors: errors, max: INTERMITTENT_ATTEMPTS, wait: INTERMITTENT_SLEEP) do
126
106
  params = options.dup
127
107
 
128
108
  if on
@@ -132,7 +112,7 @@ module Ferrum
132
112
  end
133
113
 
134
114
  if params[:executionContextId].nil? && params[:objectId].nil?
135
- params = params.merge(executionContextId: execution_id)
115
+ params = params.merge(executionContextId: execution_id!)
136
116
  end
137
117
 
138
118
  response = @page.command("Runtime.callFunctionOn",