ferrum 0.6 → 0.9
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/README.md +296 -38
- data/lib/ferrum.rb +29 -4
- data/lib/ferrum/browser.rb +14 -8
- data/lib/ferrum/browser/client.rb +19 -10
- data/lib/ferrum/browser/command.rb +57 -0
- 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 +53 -107
- 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/cookies.rb +7 -0
- data/lib/ferrum/dialog.rb +2 -2
- data/lib/ferrum/frame.rb +23 -5
- data/lib/ferrum/frame/dom.rb +38 -41
- data/lib/ferrum/frame/runtime.rb +54 -33
- data/lib/ferrum/headers.rb +1 -1
- data/lib/ferrum/keyboard.rb +3 -3
- data/lib/ferrum/mouse.rb +14 -3
- data/lib/ferrum/network.rb +60 -16
- data/lib/ferrum/network/exchange.rb +24 -21
- data/lib/ferrum/network/intercepted_request.rb +12 -3
- data/lib/ferrum/network/response.rb +4 -0
- data/lib/ferrum/node.rb +59 -26
- data/lib/ferrum/page.rb +45 -17
- data/lib/ferrum/page/frames.rb +11 -19
- data/lib/ferrum/page/screenshot.rb +3 -3
- data/lib/ferrum/target.rb +10 -2
- data/lib/ferrum/version.rb +1 -1
- metadata +10 -5
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
@@ -3,6 +3,9 @@
|
|
3
3
|
require "ferrum/network/exchange"
|
4
4
|
require "ferrum/network/intercepted_request"
|
5
5
|
require "ferrum/network/auth_request"
|
6
|
+
require "ferrum/network/error"
|
7
|
+
require "ferrum/network/request"
|
8
|
+
require "ferrum/network/response"
|
6
9
|
|
7
10
|
module Ferrum
|
8
11
|
class Network
|
@@ -20,6 +23,31 @@ module Ferrum
|
|
20
23
|
@exchange = nil
|
21
24
|
end
|
22
25
|
|
26
|
+
def wait_for_idle(connections: 0, duration: 0.05, timeout: @page.browser.timeout)
|
27
|
+
start = Ferrum.monotonic_time
|
28
|
+
|
29
|
+
until idle?(connections)
|
30
|
+
raise TimeoutError if Ferrum.timeout?(start, timeout)
|
31
|
+
sleep(duration)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def idle?(connections = 0)
|
36
|
+
pending_connections <= connections
|
37
|
+
end
|
38
|
+
|
39
|
+
def total_connections
|
40
|
+
@traffic.size
|
41
|
+
end
|
42
|
+
|
43
|
+
def finished_connections
|
44
|
+
@traffic.count(&:finished?)
|
45
|
+
end
|
46
|
+
|
47
|
+
def pending_connections
|
48
|
+
total_connections - finished_connections
|
49
|
+
end
|
50
|
+
|
23
51
|
def request
|
24
52
|
@exchange&.request
|
25
53
|
end
|
@@ -87,27 +115,43 @@ module Ferrum
|
|
87
115
|
|
88
116
|
def subscribe
|
89
117
|
@page.on("Network.requestWillBeSent") do |params|
|
118
|
+
request = Network::Request.new(params)
|
119
|
+
|
120
|
+
# We can build exchange in two places, here on the event or when request
|
121
|
+
# is interrupted. So we have to be careful when to create new one. We
|
122
|
+
# create new exchange only if there's no with such id or there's but
|
123
|
+
# it's filled with request which means this one is new but has response
|
124
|
+
# for a redirect. So we assign response from the params to previous
|
125
|
+
# exchange and build new exchange to assign this request to it.
|
126
|
+
exchange = select(request.id).last
|
127
|
+
exchange = build_exchange(request.id) unless exchange&.blank?
|
128
|
+
|
90
129
|
# On redirects Chrome doesn't change `requestId` and there's no
|
91
130
|
# `Network.responseReceived` event for such request. If there's already
|
92
131
|
# exchange object with this id then we got redirected and params has
|
93
132
|
# `redirectResponse` key which contains the response.
|
94
|
-
if
|
95
|
-
|
133
|
+
if params["redirectResponse"]
|
134
|
+
previous_exchange = select(request.id)[-2]
|
135
|
+
response = Network::Response.new(@page, params)
|
136
|
+
previous_exchange.response = response
|
96
137
|
end
|
97
138
|
|
98
|
-
exchange =
|
99
|
-
|
100
|
-
|
139
|
+
exchange.request = request
|
140
|
+
|
141
|
+
if exchange.navigation_request?(@page.main_frame.id)
|
142
|
+
@exchange = exchange
|
143
|
+
end
|
101
144
|
end
|
102
145
|
|
103
146
|
@page.on("Network.responseReceived") do |params|
|
104
|
-
if exchange =
|
105
|
-
|
147
|
+
if exchange = select(params["requestId"]).last
|
148
|
+
response = Network::Response.new(@page, params)
|
149
|
+
exchange.response = response
|
106
150
|
end
|
107
151
|
end
|
108
152
|
|
109
153
|
@page.on("Network.loadingFinished") do |params|
|
110
|
-
exchange =
|
154
|
+
exchange = select(params["requestId"]).last
|
111
155
|
if exchange && exchange.response
|
112
156
|
exchange.response.body_size = params["encodedDataLength"]
|
113
157
|
end
|
@@ -115,10 +159,10 @@ module Ferrum
|
|
115
159
|
|
116
160
|
@page.on("Log.entryAdded") do |params|
|
117
161
|
entry = params["entry"] || {}
|
118
|
-
if entry["source"] == "network" &&
|
119
|
-
|
120
|
-
|
121
|
-
exchange.
|
162
|
+
if entry["source"] == "network" && entry["level"] == "error"
|
163
|
+
exchange = select(entry["networkRequestId"]).last
|
164
|
+
error = Network::Error.new(entry)
|
165
|
+
exchange.error = error
|
122
166
|
end
|
123
167
|
end
|
124
168
|
end
|
@@ -135,12 +179,12 @@ module Ferrum
|
|
135
179
|
end
|
136
180
|
end
|
137
181
|
|
138
|
-
def
|
139
|
-
@traffic.
|
182
|
+
def select(request_id)
|
183
|
+
@traffic.select { |e| e.id == request_id }
|
140
184
|
end
|
141
185
|
|
142
|
-
def
|
143
|
-
@
|
186
|
+
def build_exchange(id)
|
187
|
+
Network::Exchange.new(@page, id).tap { |e| @traffic << e }
|
144
188
|
end
|
145
189
|
end
|
146
190
|
end
|
@@ -1,39 +1,37 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "ferrum/network/error"
|
4
|
-
require "ferrum/network/request"
|
5
|
-
require "ferrum/network/response"
|
6
|
-
|
7
3
|
module Ferrum
|
8
4
|
class Network
|
9
5
|
class Exchange
|
10
|
-
attr_reader :
|
6
|
+
attr_reader :id
|
7
|
+
attr_accessor :intercepted_request
|
8
|
+
attr_accessor :request, :response, :error
|
11
9
|
|
12
|
-
def initialize(page,
|
13
|
-
@page = page
|
14
|
-
@
|
15
|
-
|
10
|
+
def initialize(page, id)
|
11
|
+
@page, @id = page, id
|
12
|
+
@intercepted_request = nil
|
13
|
+
@request = @response = @error = nil
|
16
14
|
end
|
17
15
|
|
18
|
-
def
|
19
|
-
|
16
|
+
def navigation_request?(frame_id)
|
17
|
+
request.type?(:document) &&
|
18
|
+
request.frame_id == frame_id
|
20
19
|
end
|
21
20
|
|
22
|
-
def
|
23
|
-
|
21
|
+
def blank?
|
22
|
+
!request
|
24
23
|
end
|
25
24
|
|
26
|
-
def
|
27
|
-
|
25
|
+
def blocked?
|
26
|
+
intercepted_request && intercepted_request.status?(:aborted)
|
28
27
|
end
|
29
28
|
|
30
|
-
def
|
31
|
-
|
32
|
-
request.frame_id == frame_id
|
29
|
+
def finished?
|
30
|
+
blocked? || response || error
|
33
31
|
end
|
34
32
|
|
35
|
-
def
|
36
|
-
|
33
|
+
def pending?
|
34
|
+
!finished?
|
37
35
|
end
|
38
36
|
|
39
37
|
def to_a
|
@@ -41,7 +39,12 @@ module Ferrum
|
|
41
39
|
end
|
42
40
|
|
43
41
|
def inspect
|
44
|
-
|
42
|
+
"#<#{self.class} "\
|
43
|
+
"@id=#{@id.inspect} "\
|
44
|
+
"@intercepted_request=#{@intercepted_request.inspect} "\
|
45
|
+
"@request=#{@request.inspect} "\
|
46
|
+
"@response=#{@response.inspect} "\
|
47
|
+
"@error=#{@error.inspect}>"
|
45
48
|
end
|
46
49
|
end
|
47
50
|
end
|
@@ -5,14 +5,20 @@ require "base64"
|
|
5
5
|
module Ferrum
|
6
6
|
class Network
|
7
7
|
class InterceptedRequest
|
8
|
-
attr_accessor :request_id, :frame_id, :resource_type
|
8
|
+
attr_accessor :request_id, :frame_id, :resource_type, :network_id, :status
|
9
9
|
|
10
10
|
def initialize(page, params)
|
11
|
+
@status = nil
|
11
12
|
@page, @params = page, params
|
12
13
|
@request_id = params["requestId"]
|
13
14
|
@frame_id = params["frameId"]
|
14
15
|
@resource_type = params["resourceType"]
|
15
16
|
@request = params["request"]
|
17
|
+
@network_id = params["networkId"]
|
18
|
+
end
|
19
|
+
|
20
|
+
def status?(value)
|
21
|
+
@status == value.to_sym
|
16
22
|
end
|
17
23
|
|
18
24
|
def navigation_request?
|
@@ -25,7 +31,7 @@ module Ferrum
|
|
25
31
|
|
26
32
|
def respond(**options)
|
27
33
|
has_body = options.has_key?(:body)
|
28
|
-
headers = has_body ? { "content-length" => options.fetch(:body,
|
34
|
+
headers = has_body ? { "content-length" => options.fetch(:body, "").length } : {}
|
29
35
|
headers = headers.merge(options.fetch(:responseHeaders, {}))
|
30
36
|
|
31
37
|
options = {responseCode: 200}.merge(options)
|
@@ -33,17 +39,20 @@ module Ferrum
|
|
33
39
|
requestId: request_id,
|
34
40
|
responseHeaders: header_array(headers),
|
35
41
|
})
|
36
|
-
options = options.merge(body: Base64.
|
42
|
+
options = options.merge(body: Base64.strict_encode64(options.fetch(:body, ""))) if has_body
|
37
43
|
|
44
|
+
@status = :responded
|
38
45
|
@page.command("Fetch.fulfillRequest", **options)
|
39
46
|
end
|
40
47
|
|
41
48
|
def continue(**options)
|
42
49
|
options = options.merge(requestId: request_id)
|
50
|
+
@status = :continued
|
43
51
|
@page.command("Fetch.continueRequest", **options)
|
44
52
|
end
|
45
53
|
|
46
54
|
def abort
|
55
|
+
@status = :aborted
|
47
56
|
@page.command("Fetch.failRequest", requestId: request_id, errorReason: "BlockedByClient")
|
48
57
|
end
|
49
58
|
|
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,64 @@ 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
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
def get_content_quads
|
149
|
+
quads = page.command("DOM.getContentQuads", nodeId: node_id)["quads"]
|
150
|
+
raise "Node is either not visible or not an HTMLElement" if quads.size == 0
|
151
|
+
quads
|
152
|
+
end
|
153
|
+
|
154
|
+
def get_position(points, offset_x, offset_y, position)
|
155
|
+
x = y = nil
|
125
156
|
|
126
|
-
if offset_x
|
127
|
-
point =
|
128
|
-
|
157
|
+
if offset_x && offset_y && position == :top
|
158
|
+
point = points.first
|
159
|
+
x = point[:x] + offset_x.to_i
|
160
|
+
y = point[:y] + offset_y.to_i
|
129
161
|
else
|
130
|
-
x, y =
|
162
|
+
x, y = points.inject([0, 0]) do |memo, point|
|
131
163
|
[memo[0] + point[:x],
|
132
164
|
memo[1] + point[:y]]
|
133
165
|
end
|
134
|
-
|
166
|
+
|
167
|
+
x = x / 4
|
168
|
+
y = y / 4
|
135
169
|
end
|
136
|
-
end
|
137
170
|
|
138
|
-
|
171
|
+
if offset_x && offset_y && position == :center
|
172
|
+
x = x + offset_x.to_i
|
173
|
+
y = y + offset_y.to_i
|
174
|
+
end
|
139
175
|
|
140
|
-
|
141
|
-
|
142
|
-
raise "Node is either not visible or not an HTMLElement" if result["quads"].size == 0
|
176
|
+
[x, y]
|
177
|
+
end
|
143
178
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
{x: quad[6], y: quad[7]}]
|
150
|
-
end.first
|
179
|
+
def to_points(quad)
|
180
|
+
[{x: quad[0], y: quad[1]},
|
181
|
+
{x: quad[2], y: quad[3]},
|
182
|
+
{x: quad[4], y: quad[5]},
|
183
|
+
{x: quad[6], y: quad[7]}]
|
151
184
|
end
|
152
185
|
end
|
153
186
|
end
|