ferrum 0.6 → 0.9
Sign up to get free protection for your applications and to get access to all the features.
- 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
|