ferrum 0.7 → 0.10.2
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.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +325 -86
- data/lib/ferrum.rb +35 -7
- data/lib/ferrum/browser.rb +14 -10
- data/lib/ferrum/browser/client.rb +8 -6
- data/lib/ferrum/browser/command.rb +26 -25
- data/lib/ferrum/browser/options/base.rb +46 -0
- data/lib/ferrum/browser/options/chrome.rb +73 -0
- data/lib/ferrum/browser/options/firefox.rb +34 -0
- data/lib/ferrum/browser/process.rb +24 -12
- data/lib/ferrum/browser/subscriber.rb +5 -1
- data/lib/ferrum/browser/web_socket.rb +23 -4
- data/lib/ferrum/browser/xvfb.rb +37 -0
- data/lib/ferrum/context.rb +3 -3
- data/lib/ferrum/cookies.rb +7 -0
- data/lib/ferrum/dialog.rb +2 -2
- data/lib/ferrum/frame.rb +20 -3
- data/lib/ferrum/frame/dom.rb +34 -37
- data/lib/ferrum/frame/runtime.rb +90 -84
- data/lib/ferrum/keyboard.rb +3 -3
- data/lib/ferrum/mouse.rb +14 -3
- data/lib/ferrum/network.rb +23 -6
- data/lib/ferrum/network/error.rb +8 -15
- data/lib/ferrum/network/intercepted_request.rb +1 -1
- data/lib/ferrum/node.rb +70 -26
- data/lib/ferrum/page.rb +45 -17
- data/lib/ferrum/page/frames.rb +17 -3
- data/lib/ferrum/page/screenshot.rb +64 -12
- data/lib/ferrum/rbga.rb +38 -0
- data/lib/ferrum/version.rb +1 -1
- metadata +12 -9
- data/lib/ferrum/browser/chrome.rb +0 -76
- data/lib/ferrum/browser/firefox.rb +0 -34
data/lib/ferrum/keyboard.rb
CHANGED
@@ -33,13 +33,13 @@ module Ferrum
|
|
33
33
|
def down(key)
|
34
34
|
key = normalize_keys(Array(key))
|
35
35
|
type = key[:text] ? "keyDown" : "rawKeyDown"
|
36
|
-
@page.command("Input.dispatchKeyEvent", type: type, **key)
|
36
|
+
@page.command("Input.dispatchKeyEvent", slowmoable: true, type: type, **key)
|
37
37
|
self
|
38
38
|
end
|
39
39
|
|
40
40
|
def up(key)
|
41
41
|
key = normalize_keys(Array(key))
|
42
|
-
@page.command("Input.dispatchKeyEvent", type: "keyUp", **key)
|
42
|
+
@page.command("Input.dispatchKeyEvent", slowmoable: true, type: "keyUp", **key)
|
43
43
|
self
|
44
44
|
end
|
45
45
|
|
@@ -49,7 +49,7 @@ module Ferrum
|
|
49
49
|
keys.each do |key|
|
50
50
|
type = key[:text] ? "keyDown" : "rawKeyDown"
|
51
51
|
@page.command("Input.dispatchKeyEvent", type: type, **key)
|
52
|
-
@page.command("Input.dispatchKeyEvent", type: "keyUp", **key)
|
52
|
+
@page.command("Input.dispatchKeyEvent", slowmoable: true, type: "keyUp", **key)
|
53
53
|
end
|
54
54
|
|
55
55
|
self
|
data/lib/ferrum/mouse.rb
CHANGED
@@ -32,10 +32,21 @@ module Ferrum
|
|
32
32
|
tap { mouse_event(type: "mouseReleased", **options) }
|
33
33
|
end
|
34
34
|
|
35
|
-
# FIXME: steps
|
36
35
|
def move(x:, y:, steps: 1)
|
36
|
+
from_x, from_y = @x, @y
|
37
37
|
@x, @y = x, y
|
38
|
-
|
38
|
+
|
39
|
+
steps.times do |i|
|
40
|
+
new_x = from_x + (@x - from_x) * ((i + 1) / steps.to_f)
|
41
|
+
new_y = from_y + (@y - from_y) * ((i + 1) / steps.to_f)
|
42
|
+
|
43
|
+
@page.command("Input.dispatchMouseEvent",
|
44
|
+
slowmoable: true,
|
45
|
+
type: "mouseMoved",
|
46
|
+
x: new_x.to_i,
|
47
|
+
y: new_y.to_i)
|
48
|
+
end
|
49
|
+
|
39
50
|
self
|
40
51
|
end
|
41
52
|
|
@@ -45,7 +56,7 @@ module Ferrum
|
|
45
56
|
button = validate_button(button)
|
46
57
|
options = { x: @x, y: @y, type: type, button: button, clickCount: count }
|
47
58
|
options.merge!(modifiers: modifiers) if modifiers
|
48
|
-
@page.command("Input.dispatchMouseEvent", wait: wait, **options)
|
59
|
+
@page.command("Input.dispatchMouseEvent", wait: wait, slowmoable: true, **options)
|
49
60
|
end
|
50
61
|
|
51
62
|
def validate_button(button)
|
data/lib/ferrum/network.rb
CHANGED
@@ -83,19 +83,21 @@ module Ferrum
|
|
83
83
|
@page.command("Fetch.enable", handleAuthRequests: true, patterns: [pattern])
|
84
84
|
end
|
85
85
|
|
86
|
-
def authorize(user:, password:, type: :server)
|
86
|
+
def authorize(user:, password:, type: :server, &block)
|
87
87
|
unless AUTHORIZE_TYPE.include?(type)
|
88
88
|
raise ArgumentError, ":type should be in #{AUTHORIZE_TYPE}"
|
89
89
|
end
|
90
90
|
|
91
|
+
if !block_given? && !@page.subscribed?("Fetch.requestPaused")
|
92
|
+
raise ArgumentError, "Block is missing, call `authorize(...) { |r| r.continue } or subscribe to `on(:request)` events before calling it"
|
93
|
+
end
|
94
|
+
|
91
95
|
@authorized_ids ||= {}
|
92
96
|
@authorized_ids[type] ||= []
|
93
97
|
|
94
98
|
intercept
|
95
99
|
|
96
|
-
@page.on(:request)
|
97
|
-
request.continue
|
98
|
-
end
|
100
|
+
@page.on(:request, &block)
|
99
101
|
|
100
102
|
@page.on(:auth) do |request, index, total|
|
101
103
|
if request.auth_challenge?(type)
|
@@ -157,12 +159,27 @@ module Ferrum
|
|
157
159
|
end
|
158
160
|
end
|
159
161
|
|
162
|
+
@page.on("Network.loadingFailed") do |params|
|
163
|
+
exchange = select(params["requestId"]).last
|
164
|
+
exchange.error ||= Network::Error.new
|
165
|
+
|
166
|
+
exchange.error.id = params["requestId"]
|
167
|
+
exchange.error.type = params["type"]
|
168
|
+
exchange.error.error_text = params["errorText"]
|
169
|
+
exchange.error.monotonic_time = params["timestamp"]
|
170
|
+
exchange.error.canceled = params["canceled"]
|
171
|
+
end
|
172
|
+
|
160
173
|
@page.on("Log.entryAdded") do |params|
|
161
174
|
entry = params["entry"] || {}
|
162
175
|
if entry["source"] == "network" && entry["level"] == "error"
|
163
176
|
exchange = select(entry["networkRequestId"]).last
|
164
|
-
error
|
165
|
-
|
177
|
+
exchange.error ||= Network::Error.new
|
178
|
+
|
179
|
+
exchange.error.id = entry["networkRequestId"]
|
180
|
+
exchange.error.url = entry["url"]
|
181
|
+
exchange.error.description = entry["text"]
|
182
|
+
exchange.error.timestamp = entry["timestamp"]
|
166
183
|
end
|
167
184
|
end
|
168
185
|
end
|
data/lib/ferrum/network/error.rb
CHANGED
@@ -3,24 +3,17 @@
|
|
3
3
|
module Ferrum
|
4
4
|
class Network
|
5
5
|
class Error
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
def id
|
11
|
-
@data["networkRequestId"]
|
12
|
-
end
|
13
|
-
|
14
|
-
def url
|
15
|
-
@data["url"]
|
16
|
-
end
|
6
|
+
attr_writer :canceled
|
7
|
+
attr_reader :time, :timestamp
|
8
|
+
attr_accessor :id, :url, :type, :error_text, :monotonic_time, :description
|
17
9
|
|
18
|
-
def
|
19
|
-
@
|
10
|
+
def canceled?
|
11
|
+
@canceled
|
20
12
|
end
|
21
13
|
|
22
|
-
def
|
23
|
-
@
|
14
|
+
def timestamp=(value)
|
15
|
+
@timestamp = value
|
16
|
+
@time = Time.strptime((value / 1000).to_s, "%s")
|
24
17
|
end
|
25
18
|
end
|
26
19
|
end
|
@@ -39,7 +39,7 @@ module Ferrum
|
|
39
39
|
requestId: request_id,
|
40
40
|
responseHeaders: header_array(headers),
|
41
41
|
})
|
42
|
-
options = options.merge(body: Base64.
|
42
|
+
options = options.merge(body: Base64.strict_encode64(options.fetch(:body, ""))) if has_body
|
43
43
|
|
44
44
|
@status = :responded
|
45
45
|
@page.command("Fetch.fulfillRequest", **options)
|
data/lib/ferrum/node.rb
CHANGED
@@ -2,6 +2,9 @@
|
|
2
2
|
|
3
3
|
module Ferrum
|
4
4
|
class Node
|
5
|
+
MOVING_WAIT = ENV.fetch("FERRUM_NODE_MOVING_WAIT", 0.01).to_f
|
6
|
+
MOVING_ATTEMPTS = ENV.fetch("FERRUM_NODE_MOVING_ATTEMPTS", 50).to_i
|
7
|
+
|
5
8
|
attr_reader :page, :target_id, :node_id, :description, :tag_name
|
6
9
|
|
7
10
|
def initialize(frame, target_id, node_id, description)
|
@@ -24,7 +27,7 @@ module Ferrum
|
|
24
27
|
end
|
25
28
|
|
26
29
|
def focus
|
27
|
-
tap { page.command("DOM.focus", nodeId: node_id) }
|
30
|
+
tap { page.command("DOM.focus", slowmoable: true, nodeId: node_id) }
|
28
31
|
end
|
29
32
|
|
30
33
|
def blur
|
@@ -37,22 +40,23 @@ module Ferrum
|
|
37
40
|
|
38
41
|
# mode: (:left | :right | :double)
|
39
42
|
# keys: (:alt, (:ctrl | :control), (:meta | :command), :shift)
|
40
|
-
# offset: { :x, :y }
|
41
|
-
def click(mode: :left, keys: [], offset: {})
|
42
|
-
x, y = find_position(offset
|
43
|
+
# offset: { :x, :y, :position (:top | :center) }
|
44
|
+
def click(mode: :left, keys: [], offset: {}, delay: 0)
|
45
|
+
x, y = find_position(**offset)
|
43
46
|
modifiers = page.keyboard.modifiers(keys)
|
44
47
|
|
45
48
|
case mode
|
46
49
|
when :right
|
47
50
|
page.mouse.move(x: x, y: y)
|
48
51
|
page.mouse.down(button: :right, modifiers: modifiers)
|
52
|
+
sleep(delay)
|
49
53
|
page.mouse.up(button: :right, modifiers: modifiers)
|
50
54
|
when :double
|
51
55
|
page.mouse.move(x: x, y: y)
|
52
56
|
page.mouse.down(modifiers: modifiers, count: 2)
|
53
57
|
page.mouse.up(modifiers: modifiers, count: 2)
|
54
58
|
when :left
|
55
|
-
page.mouse.click(x: x, y: y, modifiers: modifiers)
|
59
|
+
page.mouse.click(x: x, y: y, modifiers: modifiers, delay: delay)
|
56
60
|
end
|
57
61
|
|
58
62
|
self
|
@@ -63,7 +67,7 @@ module Ferrum
|
|
63
67
|
end
|
64
68
|
|
65
69
|
def select_file(value)
|
66
|
-
page.command("DOM.setFileInputFiles", nodeId: node_id, files: Array(value))
|
70
|
+
page.command("DOM.setFileInputFiles", slowmoable: true, nodeId: node_id, files: Array(value))
|
67
71
|
end
|
68
72
|
|
69
73
|
def at_xpath(selector)
|
@@ -119,35 +123,75 @@ module Ferrum
|
|
119
123
|
%(#<#{self.class} @target_id=#{@target_id.inspect} @node_id=#{@node_id} @description=#{@description.inspect}>)
|
120
124
|
end
|
121
125
|
|
122
|
-
def find_position(
|
123
|
-
|
124
|
-
|
126
|
+
def find_position(x: nil, y: nil, position: :top)
|
127
|
+
prev = get_content_quads
|
128
|
+
|
129
|
+
# FIXME: Case when a few quads returned
|
130
|
+
points = Ferrum.with_attempts(errors: NodeIsMovingError, max: MOVING_ATTEMPTS, wait: 0) do
|
131
|
+
sleep(MOVING_WAIT)
|
132
|
+
current = get_content_quads
|
133
|
+
|
134
|
+
if current != prev
|
135
|
+
error = NodeIsMovingError.new(self, prev, current)
|
136
|
+
prev = current
|
137
|
+
raise(error)
|
138
|
+
end
|
139
|
+
|
140
|
+
current
|
141
|
+
end.map { |q| to_points(q) }.first
|
142
|
+
|
143
|
+
get_position(points, x, y, position)
|
144
|
+
rescue Ferrum::BrowserError => e
|
145
|
+
return raise unless e.message&.include?("Could not compute content quads")
|
146
|
+
|
147
|
+
find_position_via_js
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
def find_position_via_js
|
153
|
+
[
|
154
|
+
evaluate("this.getBoundingClientRect().left + window.pageXOffset + (this.offsetWidth / 2)"), # x
|
155
|
+
evaluate("this.getBoundingClientRect().top + window.pageYOffset + (this.offsetHeight / 2)") # y
|
156
|
+
]
|
157
|
+
end
|
125
158
|
|
126
|
-
|
127
|
-
|
128
|
-
|
159
|
+
def get_content_quads
|
160
|
+
quads = page.command("DOM.getContentQuads", nodeId: node_id)["quads"]
|
161
|
+
raise "Node is either not visible or not an HTMLElement" if quads.size == 0
|
162
|
+
quads
|
163
|
+
end
|
164
|
+
|
165
|
+
def get_position(points, offset_x, offset_y, position)
|
166
|
+
x = y = nil
|
167
|
+
|
168
|
+
if offset_x && offset_y && position == :top
|
169
|
+
point = points.first
|
170
|
+
x = point[:x] + offset_x.to_i
|
171
|
+
y = point[:y] + offset_y.to_i
|
129
172
|
else
|
130
|
-
x, y =
|
173
|
+
x, y = points.inject([0, 0]) do |memo, point|
|
131
174
|
[memo[0] + point[:x],
|
132
175
|
memo[1] + point[:y]]
|
133
176
|
end
|
134
|
-
|
177
|
+
|
178
|
+
x = x / 4
|
179
|
+
y = y / 4
|
135
180
|
end
|
136
|
-
end
|
137
181
|
|
138
|
-
|
182
|
+
if offset_x && offset_y && position == :center
|
183
|
+
x = x + offset_x.to_i
|
184
|
+
y = y + offset_y.to_i
|
185
|
+
end
|
139
186
|
|
140
|
-
|
141
|
-
|
142
|
-
raise "Node is either not visible or not an HTMLElement" if result["quads"].size == 0
|
187
|
+
[x, y]
|
188
|
+
end
|
143
189
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
{x: quad[6], y: quad[7]}]
|
150
|
-
end.first
|
190
|
+
def to_points(quad)
|
191
|
+
[{x: quad[0], y: quad[1]},
|
192
|
+
{x: quad[2], y: quad[3]},
|
193
|
+
{x: quad[4], y: quad[5]},
|
194
|
+
{x: quad[6], y: quad[7]}]
|
151
195
|
end
|
152
196
|
end
|
153
197
|
end
|
data/lib/ferrum/page.rb
CHANGED
@@ -31,8 +31,8 @@ module Ferrum
|
|
31
31
|
|
32
32
|
extend Forwardable
|
33
33
|
delegate %i[at_css at_xpath css xpath
|
34
|
-
current_url current_title url title body doctype
|
35
|
-
execution_id evaluate evaluate_on evaluate_async execute
|
34
|
+
current_url current_title url title body doctype set_content
|
35
|
+
execution_id evaluate evaluate_on evaluate_async execute evaluate_func
|
36
36
|
add_script_tag add_style_tag] => :main_frame
|
37
37
|
|
38
38
|
include Frames, Screenshot
|
@@ -44,13 +44,14 @@ module Ferrum
|
|
44
44
|
|
45
45
|
def initialize(target_id, browser)
|
46
46
|
@frames = {}
|
47
|
+
@main_frame = Frame.new(nil, self)
|
47
48
|
@target_id, @browser = target_id, browser
|
48
49
|
@event = Event.new.tap(&:set)
|
49
50
|
|
50
51
|
host = @browser.process.host
|
51
52
|
port = @browser.process.port
|
52
53
|
ws_url = "ws://#{host}:#{port}/devtools/page/#{@target_id}"
|
53
|
-
@client = Browser::Client.new(browser, ws_url, 1000)
|
54
|
+
@client = Browser::Client.new(browser, ws_url, id_starts_with: 1000)
|
54
55
|
|
55
56
|
@mouse, @keyboard = Mouse.new(self), Keyboard.new(self)
|
56
57
|
@headers, @cookies = Headers.new(self), Cookies.new(self)
|
@@ -64,7 +65,7 @@ module Ferrum
|
|
64
65
|
@browser.timeout
|
65
66
|
end
|
66
67
|
|
67
|
-
def
|
68
|
+
def go_to(url = nil)
|
68
69
|
options = { url: combine_url!(url) }
|
69
70
|
options.merge!(referrer: referrer) if referrer
|
70
71
|
response = command("Page.navigate", wait: GOTO_WAIT, **options)
|
@@ -77,9 +78,12 @@ module Ferrum
|
|
77
78
|
end
|
78
79
|
response["frameId"]
|
79
80
|
rescue TimeoutError
|
80
|
-
|
81
|
-
|
81
|
+
if @browser.pending_connection_errors
|
82
|
+
pendings = network.traffic.select(&:pending?).map { |e| e.request.url }
|
83
|
+
raise PendingConnectionsError.new(options[:url], pendings) unless pendings.empty?
|
84
|
+
end
|
82
85
|
end
|
86
|
+
alias goto go_to
|
83
87
|
|
84
88
|
def close
|
85
89
|
@headers.clear
|
@@ -88,18 +92,16 @@ module Ferrum
|
|
88
92
|
end
|
89
93
|
|
90
94
|
def resize(width: nil, height: nil, fullscreen: false)
|
91
|
-
result = @browser.command("Browser.getWindowForTarget", targetId: @target_id)
|
92
|
-
@window_id, @bounds = result.values_at("windowId", "bounds")
|
93
|
-
|
94
95
|
if fullscreen
|
95
96
|
width, height = document_size
|
96
|
-
|
97
|
+
set_window_bounds(windowState: "fullscreen")
|
97
98
|
else
|
98
|
-
|
99
|
-
|
99
|
+
set_window_bounds(windowState: "normal")
|
100
|
+
set_window_bounds(width: width, height: height)
|
100
101
|
end
|
101
102
|
|
102
|
-
command("Emulation.setDeviceMetricsOverride",
|
103
|
+
command("Emulation.setDeviceMetricsOverride", slowmoable: true,
|
104
|
+
width: width,
|
103
105
|
height: height,
|
104
106
|
deviceScaleFactor: 1,
|
105
107
|
mobile: false,
|
@@ -107,10 +109,14 @@ module Ferrum
|
|
107
109
|
end
|
108
110
|
|
109
111
|
def refresh
|
110
|
-
command("Page.reload", wait: timeout)
|
112
|
+
command("Page.reload", wait: timeout, slowmoable: true)
|
111
113
|
end
|
112
114
|
alias_method :reload, :refresh
|
113
115
|
|
116
|
+
def stop
|
117
|
+
command("Page.stopLoading", slowmoable: true)
|
118
|
+
end
|
119
|
+
|
114
120
|
def back
|
115
121
|
history_navigate(delta: -1)
|
116
122
|
end
|
@@ -119,15 +125,31 @@ module Ferrum
|
|
119
125
|
history_navigate(delta: 1)
|
120
126
|
end
|
121
127
|
|
128
|
+
def wait_for_reload(sec = 1)
|
129
|
+
@event.reset if @event.set?
|
130
|
+
@event.wait(sec)
|
131
|
+
@event.set
|
132
|
+
end
|
133
|
+
|
122
134
|
def bypass_csp(value = true)
|
123
135
|
enabled = !!value
|
124
136
|
command("Page.setBypassCSP", enabled: enabled)
|
125
137
|
enabled
|
126
138
|
end
|
127
139
|
|
128
|
-
def
|
140
|
+
def window_id
|
141
|
+
@browser.command("Browser.getWindowForTarget", targetId: @target_id)["windowId"]
|
142
|
+
end
|
143
|
+
|
144
|
+
def set_window_bounds(bounds = {})
|
145
|
+
@browser.command("Browser.setWindowBounds", windowId: window_id, bounds: bounds)
|
146
|
+
end
|
147
|
+
|
148
|
+
def command(method, wait: 0, slowmoable: false, **params)
|
129
149
|
iteration = @event.reset if wait > 0
|
150
|
+
sleep(@browser.slowmo) if slowmoable && @browser.slowmo > 0
|
130
151
|
result = @client.command(method, params)
|
152
|
+
|
131
153
|
if wait > 0
|
132
154
|
@event.wait(wait) # Wait a bit after command and check if iteration has
|
133
155
|
# changed which means there was some network event for
|
@@ -165,6 +187,10 @@ module Ferrum
|
|
165
187
|
end
|
166
188
|
end
|
167
189
|
|
190
|
+
def subscribed?(event)
|
191
|
+
@client.subscribed?(event)
|
192
|
+
end
|
193
|
+
|
168
194
|
private
|
169
195
|
|
170
196
|
def subscribe
|
@@ -236,7 +262,9 @@ module Ferrum
|
|
236
262
|
|
237
263
|
if entry = entries[index + delta]
|
238
264
|
# Potential wait because of network event
|
239
|
-
command("Page.navigateToHistoryEntry", wait: Mouse::CLICK_WAIT,
|
265
|
+
command("Page.navigateToHistoryEntry", wait: Mouse::CLICK_WAIT,
|
266
|
+
slowmoable: true,
|
267
|
+
entryId: entry["id"])
|
240
268
|
end
|
241
269
|
end
|
242
270
|
|
@@ -245,7 +273,7 @@ module Ferrum
|
|
245
273
|
nil_or_relative = url.nil? || url.relative?
|
246
274
|
|
247
275
|
if nil_or_relative && !@browser.base_url
|
248
|
-
raise "Set :base_url browser's option or use absolute url in `
|
276
|
+
raise "Set :base_url browser's option or use absolute url in `go_to`, you passed: #{url_or_path}"
|
249
277
|
end
|
250
278
|
|
251
279
|
(nil_or_relative ? @browser.base_url.join(url.to_s) : url).to_s
|