ferrum 0.12 → 0.14

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