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
data/lib/ferrum/frame.rb CHANGED
@@ -14,8 +14,30 @@ module Ferrum
14
14
  stopped_loading
15
15
  ].freeze
16
16
 
17
- attr_accessor :id, :name
18
- attr_reader :page, :parent_id, :state
17
+ # The Frame's unique id.
18
+ #
19
+ # @return [String]
20
+ attr_accessor :id
21
+
22
+ # If frame was given a name it should be here.
23
+ #
24
+ # @return [String, nil]
25
+ attr_accessor :name
26
+
27
+ # The page the frame belongs to.
28
+ #
29
+ # @return [Page]
30
+ attr_reader :page
31
+
32
+ # Parent frame id if this one is nested in another one.
33
+ #
34
+ # @return [String, nil]
35
+ attr_reader :parent_id
36
+
37
+ # One of the states frame's in.
38
+ #
39
+ # @return [:started_loading, :navigated, :stopped_loading, nil]
40
+ attr_reader :state
19
41
 
20
42
  def initialize(id, page, parent_id = nil)
21
43
  @id = id
@@ -30,18 +52,60 @@ module Ferrum
30
52
  @state = value
31
53
  end
32
54
 
55
+ #
56
+ # Returns current frame's `location.href`.
57
+ #
58
+ # @return [String]
59
+ #
60
+ # @example
61
+ # browser.go_to("https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe")
62
+ # frame = browser.frames[1]
63
+ # frame.url # => https://interactive-examples.mdn.mozilla.net/pages/tabbed/iframe.html
64
+ #
33
65
  def url
34
66
  evaluate("document.location.href")
35
67
  end
36
68
 
69
+ #
70
+ # Returns current frame's title.
71
+ #
72
+ # @return [String]
73
+ #
74
+ # @example
75
+ # browser.go_to("https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe")
76
+ # frame = browser.frames[1]
77
+ # frame.title # => HTML Demo: <iframe>
78
+ #
37
79
  def title
38
80
  evaluate("document.title")
39
81
  end
40
82
 
83
+ #
84
+ # If current frame is the main frame of the page (top of the tree).
85
+ #
86
+ # @return [Boolean]
87
+ #
88
+ # @example
89
+ # browser.go_to("https://www.w3schools.com/tags/tag_frame.asp")
90
+ # frame = browser.frame_by(id: "C09C4E4404314AAEAE85928EAC109A93")
91
+ # frame.main? # => false
92
+ #
41
93
  def main?
42
94
  @parent_id.nil?
43
95
  end
44
96
 
97
+ #
98
+ # Sets a content of a given frame.
99
+ #
100
+ # @param [String] html
101
+ #
102
+ # @example
103
+ # browser.go_to("https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe")
104
+ # frame = browser.frames[1]
105
+ # frame.body # <html lang="en"><head><style>body {transition: opacity ease-in 0.2s; }...
106
+ # frame.content = "<html><head></head><body><p>lol</p></body></html>"
107
+ # frame.body # => <html><head></head><body><p>lol</p></body></html>
108
+ #
45
109
  def content=(html)
46
110
  evaluate_async(%(
47
111
  document.open();
@@ -49,16 +113,40 @@ module Ferrum
49
113
  document.close();
50
114
  arguments[1](true);
51
115
  ), @page.timeout, html)
116
+ @page.document_node_id
52
117
  end
53
118
  alias set_content content=
54
119
 
55
- def execution_id
120
+ #
121
+ # Execution context id which is used by JS, each frame has it's own
122
+ # context in which JS evaluates. Locks for a page timeout and raises
123
+ # an error if an execution id hasn't been set yet, if id is set
124
+ # returns immediately.
125
+ #
126
+ # @return [Integer]
127
+ #
128
+ # @raise [NoExecutionContextError]
129
+ #
130
+ def execution_id!
56
131
  value = @execution_id.borrow(@page.timeout, &:itself)
57
132
  raise NoExecutionContextError if value.instance_of?(Object)
58
133
 
59
134
  value
60
135
  end
61
136
 
137
+ #
138
+ # Execution context id which is used by JS, each frame has it's own
139
+ # context in which JS evaluates.
140
+ #
141
+ # @return [Integer, nil]
142
+ #
143
+ def execution_id
144
+ value = @execution_id.value
145
+ return if value.instance_of?(Object)
146
+
147
+ value
148
+ end
149
+
62
150
  def execution_id=(value)
63
151
  if value.nil?
64
152
  @execution_id.try_take!
@@ -7,20 +7,48 @@ module Ferrum
7
7
  @headers = {}
8
8
  end
9
9
 
10
+ #
11
+ # Get all headers.
12
+ #
13
+ # @return [Hash{String => String}]
14
+ #
10
15
  def get
11
16
  @headers
12
17
  end
13
18
 
19
+ #
20
+ # Set given headers. Eventually clear all headers and set given ones.
21
+ #
22
+ # @param [Hash{String => String}] headers
23
+ # key-value pairs for example `"User-Agent" => "Browser"`.
24
+ #
25
+ # @return [true]
26
+ #
14
27
  def set(headers)
15
28
  clear
16
29
  add(headers)
17
30
  end
18
31
 
32
+ #
33
+ # Clear all headers.
34
+ #
35
+ # @return [true]
36
+ #
19
37
  def clear
20
38
  @headers = {}
21
39
  true
22
40
  end
23
41
 
42
+ #
43
+ # Adds given headers to already set ones.
44
+ #
45
+ # @param [Hash{String => String}] headers
46
+ # key-value pairs for example `"Referer" => "http://example.com"`.
47
+ #
48
+ # @param [Boolean] permanent
49
+ #
50
+ # @return [true]
51
+ #
24
52
  def add(headers, permanent: true)
25
53
  if headers["Referer"]
26
54
  @page.referrer = headers["Referer"]
@@ -30,19 +30,44 @@ module Ferrum
30
30
  @page = page
31
31
  end
32
32
 
33
+ #
34
+ # Dispatches a `keydown` event.
35
+ #
36
+ # @param [String, Symbol] key
37
+ # Name of the key, such as `"a"`, `:enter`, or `:backspace`.
38
+ #
39
+ # @return [self]
40
+ #
33
41
  def down(key)
34
- key = normalize_keys(Array(key))
42
+ key = normalize_keys(Array(key)).first
35
43
  type = key[:text] ? "keyDown" : "rawKeyDown"
36
44
  @page.command("Input.dispatchKeyEvent", slowmoable: true, type: type, **key)
37
45
  self
38
46
  end
39
47
 
48
+ #
49
+ # Dispatches a `keyup` event.
50
+ #
51
+ # @param [String, Symbol] key
52
+ # Name of the key, such as `"a"`, `:enter`, or `:backspace`.
53
+ #
54
+ # @return [self]
55
+ #
40
56
  def up(key)
41
- key = normalize_keys(Array(key))
57
+ key = normalize_keys(Array(key)).first
42
58
  @page.command("Input.dispatchKeyEvent", slowmoable: true, type: "keyUp", **key)
43
59
  self
44
60
  end
45
61
 
62
+ #
63
+ # Sends a keydown, keypress/input, and keyup event for each character in
64
+ # the text.
65
+ #
66
+ # @param [Array<String, Symbol, (Symbol, String)>] keys
67
+ # The text to type into a focused element, `[:Shift, "s"], "tring"`.
68
+ #
69
+ # @return [self]
70
+ #
46
71
  def type(*keys)
47
72
  keys = normalize_keys(Array(keys))
48
73
 
@@ -55,15 +80,27 @@ module Ferrum
55
80
  self
56
81
  end
57
82
 
83
+ #
84
+ # Returns bitfield for a given keys.
85
+ #
86
+ # @param [Array<:alt, :ctrl, :command, :shift>] keys
87
+ #
88
+ # @return [Integer]
89
+ #
58
90
  def modifiers(keys)
59
91
  keys.map { |k| MODIFIERS[k.to_s] }.compact.reduce(0, :|)
60
92
  end
61
93
 
62
94
  private
63
95
 
96
+ # TODO: Refactor it, and try to simplify complexity
97
+ # rubocop:disable Metrics/PerceivedComplexity
98
+ # rubocop:disable Metrics/CyclomaticComplexity
64
99
  def normalize_keys(keys, pressed_keys = [], memo = [])
65
100
  case keys
66
101
  when Array
102
+ raise ArgumentError, "empty keys passed" if keys.empty?
103
+
67
104
  pressed_keys.push([])
68
105
  memo += combine_strings(keys).map do |key|
69
106
  normalize_keys(key, pressed_keys, memo)
@@ -82,6 +119,8 @@ module Ferrum
82
119
  to_options(key)
83
120
  end
84
121
  when String
122
+ raise ArgumentError, "empty keys passed" if keys.empty?
123
+
85
124
  pressed = pressed_keys.flatten
86
125
  keys.each_char.map do |char|
87
126
  key = KEYS[char] || {}
@@ -102,8 +141,12 @@ module Ferrum
102
141
  modifiers + [to_options(key)]
103
142
  end.flatten
104
143
  end
144
+ else
145
+ raise ArgumentError, "unexpected argument"
105
146
  end
106
147
  end
148
+ # rubocop:enable Metrics/PerceivedComplexity
149
+ # rubocop:enable Metrics/CyclomaticComplexity
107
150
 
108
151
  def combine_strings(keys)
109
152
  keys
data/lib/ferrum/mouse.rb CHANGED
@@ -10,10 +10,50 @@ module Ferrum
10
10
  @x = @y = 0
11
11
  end
12
12
 
13
+ #
14
+ # Scroll page to a given x, y coordinates.
15
+ #
16
+ # @param [Integer] top
17
+ # The pixel along the horizontal axis of the document that you want
18
+ # displayed in the upper left.
19
+ #
20
+ # @param [Integer] left
21
+ # The pixel along the vertical axis of the document that you want
22
+ # displayed in the upper left.
23
+ #
24
+ # @example
25
+ # browser.go_to("https://www.google.com/search?q=Ruby+headless+driver+for+Capybara")
26
+ # browser.mouse.scroll_to(0, 400)
27
+ #
13
28
  def scroll_to(top, left)
14
29
  tap { @page.execute("window.scrollTo(#{top}, #{left})") }
15
30
  end
16
31
 
32
+ #
33
+ # Click given coordinates, fires mouse move, down and up events.
34
+ #
35
+ # @param [Integer] x
36
+ #
37
+ # @param [Integer] y
38
+ #
39
+ # @param [Float] delay
40
+ # Delay between mouse down and mouse up events.
41
+ #
42
+ # @param [Float] wait
43
+ #
44
+ # @param [Hash{Symbol => Object}] options
45
+ # Additional keyword arguments.
46
+ #
47
+ # @option options [:left, :right] :button (:left)
48
+ # The mouse button to click.
49
+ #
50
+ # @option options [Integer] :count (1)
51
+ #
52
+ # @option options [Integer] :modifiers
53
+ # Bitfield for key modifiers. See`keyboard.modifiers`.
54
+ #
55
+ # @return [self]
56
+ #
17
57
  def click(x:, y:, delay: 0, wait: CLICK_WAIT, **options)
18
58
  move(x: x, y: y)
19
59
  down(**options)
@@ -24,14 +64,58 @@ module Ferrum
24
64
  self
25
65
  end
26
66
 
67
+ #
68
+ # Mouse down for given coordinates.
69
+ #
70
+ # @param [Hash{Symbol => Object}] options
71
+ # Additional keyword arguments.
72
+ #
73
+ # @option options [:left, :right] :button (:left)
74
+ # The mouse button to click.
75
+ #
76
+ # @option options [Integer] :count (1)
77
+ #
78
+ # @option options [Integer] :modifiers
79
+ # Bitfield for key modifiers. See`keyboard.modifiers`.
80
+ #
81
+ # @return [self]
82
+ #
27
83
  def down(**options)
28
84
  tap { mouse_event(type: "mousePressed", **options) }
29
85
  end
30
86
 
87
+ #
88
+ # Mouse up for given coordinates.
89
+ #
90
+ # @param [Hash{Symbol => Object}] options
91
+ # Additional keyword arguments.
92
+ #
93
+ # @option options [:left, :right] :button (:left)
94
+ # The mouse button to click.
95
+ #
96
+ # @option options [Integer] :count (1)
97
+ #
98
+ # @option options [Integer] :modifiers
99
+ # Bitfield for key modifiers. See`keyboard.modifiers`.
100
+ #
101
+ # @return [self]
102
+ #
31
103
  def up(**options)
32
104
  tap { mouse_event(type: "mouseReleased", **options) }
33
105
  end
34
106
 
107
+ #
108
+ # Mouse move to given x and y.
109
+ #
110
+ # @param [Integer] x
111
+ #
112
+ # @param [Integer] y
113
+ #
114
+ # @param [Integer] steps
115
+ # Sends intermediate mousemove events.
116
+ #
117
+ # @return [self]
118
+ #
35
119
  def move(x:, y:, steps: 1)
36
120
  from_x = @x
37
121
  from_y = @y
@@ -3,9 +3,38 @@
3
3
  module Ferrum
4
4
  class Network
5
5
  class Exchange
6
+ # ID of the request.
7
+ #
8
+ # @return String
6
9
  attr_reader :id
7
- attr_accessor :intercepted_request, :request, :response, :error
8
10
 
11
+ # The intercepted request.
12
+ #
13
+ # @return [InterceptedRequest, nil]
14
+ attr_accessor :intercepted_request
15
+
16
+ # The request object.
17
+ #
18
+ # @return [Request, nil]
19
+ attr_accessor :request
20
+
21
+ # The response object.
22
+ #
23
+ # @return [Response, nil]
24
+ attr_accessor :response
25
+
26
+ # The error object.
27
+ #
28
+ # @return [Error, nil]
29
+ attr_accessor :error
30
+
31
+ #
32
+ # Initializes the network exchange.
33
+ #
34
+ # @param [Page] page
35
+ #
36
+ # @param [String] id
37
+ #
9
38
  def initialize(page, id)
10
39
  @id = id
11
40
  @page = page
@@ -13,35 +42,105 @@ module Ferrum
13
42
  @request = @response = @error = nil
14
43
  end
15
44
 
45
+ #
46
+ # Determines if the network exchange was caused by a page navigation
47
+ # event.
48
+ #
49
+ # @param [String] frame_id
50
+ #
51
+ # @return [Boolean]
52
+ #
16
53
  def navigation_request?(frame_id)
17
- request.type?(:document) &&
18
- request.frame_id == frame_id
54
+ request&.type?(:document) && request&.frame_id == frame_id
19
55
  end
20
56
 
57
+ #
58
+ # Determines if the network exchange has a request.
59
+ #
60
+ # @return [Boolean]
61
+ #
21
62
  def blank?
22
63
  !request
23
64
  end
24
65
 
66
+ #
67
+ # Determines if the request was intercepted and blocked.
68
+ #
69
+ # @return [Boolean]
70
+ #
25
71
  def blocked?
26
72
  intercepted? && intercepted_request.status?(:aborted)
27
73
  end
28
74
 
75
+ #
76
+ # Determines if the request was blocked, a response was returned, or if an
77
+ # error occurred.
78
+ #
79
+ # @return [Boolean]
80
+ #
29
81
  def finished?
30
- blocked? || response || error
82
+ blocked? || response&.loaded? || !error.nil?
31
83
  end
32
84
 
85
+ #
86
+ # Determines if the network exchange is still not finished.
87
+ #
88
+ # @return [Boolean]
89
+ #
33
90
  def pending?
34
91
  !finished?
35
92
  end
36
93
 
94
+ #
95
+ # Determines if the exchange's request was intercepted.
96
+ #
97
+ # @return [Boolean]
98
+ #
37
99
  def intercepted?
38
- intercepted_request
100
+ !intercepted_request.nil?
101
+ end
102
+
103
+ #
104
+ # Determines if the exchange is XHR.
105
+ #
106
+ # @return [Boolean]
107
+ #
108
+ def xhr?
109
+ !!request&.xhr?
110
+ end
111
+
112
+ #
113
+ # Determines if the exchange is a redirect.
114
+ #
115
+ # @return [Boolean]
116
+ #
117
+ def redirect?
118
+ response&.redirect?
119
+ end
120
+
121
+ #
122
+ # Returns request's URL.
123
+ #
124
+ # @return [String, nil]
125
+ #
126
+ def url
127
+ request&.url
39
128
  end
40
129
 
130
+ #
131
+ # Converts the network exchange into a request, response, and error tuple.
132
+ #
133
+ # @return [Array]
134
+ #
41
135
  def to_a
42
136
  [request, response, error]
43
137
  end
44
138
 
139
+ #
140
+ # Inspects the network exchange.
141
+ #
142
+ # @return [String]
143
+ #
45
144
  def inspect
46
145
  "#<#{self.class} " \
47
146
  "@id=#{@id.inspect} " \
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "ferrum/network/request_params"
3
4
  require "base64"
4
5
 
5
6
  module Ferrum
6
7
  class Network
7
8
  class InterceptedRequest
9
+ include RequestParams
10
+
8
11
  attr_accessor :request_id, :frame_id, :resource_type, :network_id, :status
9
12
 
10
13
  def initialize(page, params)
@@ -54,18 +57,6 @@ module Ferrum
54
57
  @page.command("Fetch.failRequest", requestId: request_id, errorReason: "BlockedByClient")
55
58
  end
56
59
 
57
- def url
58
- @request["url"]
59
- end
60
-
61
- def method
62
- @request["method"]
63
- end
64
-
65
- def headers
66
- @request["headers"]
67
- end
68
-
69
60
  def initial_priority
70
61
  @request["initialPriority"]
71
62
  end
@@ -1,55 +1,94 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "ferrum/network/request_params"
3
4
  require "time"
4
5
 
5
6
  module Ferrum
6
7
  class Network
8
+ #
9
+ # Represents a [Network.Request](https://chromedevtools.github.io/devtools-protocol/1-3/Network/#type-Request)
10
+ # object.
11
+ #
7
12
  class Request
13
+ include RequestParams
14
+
15
+ #
16
+ # Initializes the request object.
17
+ #
18
+ # @param [Hash{String => Object}] params
19
+ # The parsed JSON attributes.
20
+ #
8
21
  def initialize(params)
9
22
  @params = params
10
23
  @request = @params["request"]
11
24
  end
12
25
 
26
+ #
27
+ # The request ID.
28
+ #
29
+ # @return [String]
30
+ #
13
31
  def id
14
32
  @params["requestId"]
15
33
  end
16
34
 
35
+ #
36
+ # The request resouce type.
37
+ #
38
+ # @return [String]
39
+ #
17
40
  def type
18
41
  @params["type"]
19
42
  end
20
43
 
44
+ #
45
+ # Determines if the request is of the given type.
46
+ #
47
+ # @param [String, Symbol] value
48
+ # The type value to compare against.
49
+ #
50
+ # @return [Boolean]
51
+ #
21
52
  def type?(value)
22
53
  type.downcase == value.to_s.downcase
23
54
  end
24
55
 
25
- def frame_id
26
- @params["frameId"]
27
- end
28
-
29
- def url
30
- @request["url"]
56
+ #
57
+ # Determines if the request is XHR.
58
+ #
59
+ # @return [Boolean]
60
+ #
61
+ def xhr?
62
+ type?("xhr")
31
63
  end
32
64
 
33
- def url_fragment
34
- @request["urlFragment"]
35
- end
36
-
37
- def method
38
- @request["method"]
39
- end
40
-
41
- def headers
42
- @request["headers"]
65
+ #
66
+ # The frame ID of the request.
67
+ #
68
+ # @return [String]
69
+ #
70
+ def frame_id
71
+ @params["frameId"]
43
72
  end
44
73
 
74
+ #
75
+ # The request timestamp.
76
+ #
77
+ # @return [Time]
78
+ #
45
79
  def time
46
80
  @time ||= Time.strptime(@params["wallTime"].to_s, "%s")
47
81
  end
48
82
 
49
- def post_data
50
- @request["postData"]
83
+ #
84
+ # Converts the request to a Hash.
85
+ #
86
+ # @return [Hash{String => Object}]
87
+ # The params of the request.
88
+ #
89
+ def to_h
90
+ @params
51
91
  end
52
- alias body post_data
53
92
  end
54
93
  end
55
94
  end