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
@@ -7,9 +7,13 @@ module Ferrum
|
|
7
7
|
class Subscriber
|
8
8
|
include Concurrent::Async
|
9
9
|
|
10
|
+
def self.build(size)
|
11
|
+
(0..size).map { new }
|
12
|
+
end
|
13
|
+
|
10
14
|
def initialize
|
11
15
|
super
|
12
|
-
@on = Hash.new { |h, k| h[k] =
|
16
|
+
@on = Concurrent::Hash.new { |h, k| h[k] = Concurrent::Array.new }
|
13
17
|
end
|
14
18
|
|
15
19
|
def on(event, &block)
|
@@ -8,24 +8,32 @@ module Ferrum
|
|
8
8
|
class Browser
|
9
9
|
class WebSocket
|
10
10
|
WEBSOCKET_BUG_SLEEP = 0.01
|
11
|
+
SKIP_LOGGING_SCREENSHOTS = !ENV["FERRUM_LOGGING_SCREENSHOTS"]
|
11
12
|
|
12
13
|
attr_reader :url, :messages
|
13
14
|
|
14
|
-
def initialize(url, logger)
|
15
|
+
def initialize(url, max_receive_size, logger)
|
15
16
|
@url = url
|
16
17
|
@logger = logger
|
17
18
|
uri = URI.parse(@url)
|
18
19
|
@sock = TCPSocket.new(uri.host, uri.port)
|
19
|
-
|
20
|
+
max_receive_size ||= ::WebSocket::Driver::MAX_LENGTH
|
21
|
+
@driver = ::WebSocket::Driver.client(self, max_length: max_receive_size)
|
20
22
|
@messages = Queue.new
|
21
23
|
|
24
|
+
if SKIP_LOGGING_SCREENSHOTS
|
25
|
+
@screenshot_commands = Concurrent::Hash.new
|
26
|
+
end
|
27
|
+
|
22
28
|
@driver.on(:open, &method(:on_open))
|
23
29
|
@driver.on(:message, &method(:on_message))
|
24
30
|
@driver.on(:close, &method(:on_close))
|
25
31
|
|
26
32
|
@thread = Thread.new do
|
27
33
|
Thread.current.abort_on_exception = true
|
28
|
-
|
34
|
+
if Thread.current.respond_to?(:report_on_exception=)
|
35
|
+
Thread.current.report_on_exception = true
|
36
|
+
end
|
29
37
|
|
30
38
|
begin
|
31
39
|
while data = @sock.readpartial(512)
|
@@ -47,7 +55,14 @@ module Ferrum
|
|
47
55
|
def on_message(event)
|
48
56
|
data = JSON.parse(event.data)
|
49
57
|
@messages.push(data)
|
50
|
-
|
58
|
+
|
59
|
+
output = event.data
|
60
|
+
if SKIP_LOGGING_SCREENSHOTS && @screenshot_commands[data["id"]]
|
61
|
+
@screenshot_commands.delete(data["id"])
|
62
|
+
output.sub!(/{"data":"(.*)"}/, %("Set FERRUM_LOGGING_SCREENSHOTS=true to see screenshots in Base64"))
|
63
|
+
end
|
64
|
+
|
65
|
+
@logger&.puts(" ◀ #{Ferrum.elapsed_time} #{output}\n")
|
51
66
|
end
|
52
67
|
|
53
68
|
def on_close(_event)
|
@@ -56,6 +71,10 @@ module Ferrum
|
|
56
71
|
end
|
57
72
|
|
58
73
|
def send_message(data)
|
74
|
+
if SKIP_LOGGING_SCREENSHOTS
|
75
|
+
@screenshot_commands[data[:id]] = true
|
76
|
+
end
|
77
|
+
|
59
78
|
json = data.to_json
|
60
79
|
@driver.text(json)
|
61
80
|
@logger&.puts("\n\n▶ #{Ferrum.elapsed_time} #{json}")
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ferrum
|
4
|
+
class Browser
|
5
|
+
class Xvfb
|
6
|
+
NOT_FOUND = "Could not find an executable for the Xvfb. Try to install " \
|
7
|
+
"it with your package manager".freeze
|
8
|
+
|
9
|
+
def self.start(*args)
|
10
|
+
new(*args).tap(&:start)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.xvfb_path
|
14
|
+
Cliver.detect("Xvfb")
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :screen_size, :display_id, :pid
|
18
|
+
|
19
|
+
def initialize(options)
|
20
|
+
@path = self.class.xvfb_path
|
21
|
+
raise Cliver::Dependency::NotFound.new(NOT_FOUND) unless @path
|
22
|
+
|
23
|
+
@screen_size = options.fetch(:window_size, [1024, 768]).join("x") + "x24"
|
24
|
+
@display_id = (Time.now.to_f * 1000).to_i % 100_000_000
|
25
|
+
end
|
26
|
+
|
27
|
+
def start
|
28
|
+
@pid = ::Process.spawn("#{@path} :#{display_id} -screen 0 #{screen_size}")
|
29
|
+
::Process.detach(@pid)
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_env
|
33
|
+
{ "DISPLAY" => ":#{display_id}" }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/ferrum/cookies.rb
CHANGED
@@ -23,6 +23,10 @@ module Ferrum
|
|
23
23
|
@attributes["path"]
|
24
24
|
end
|
25
25
|
|
26
|
+
def samesite
|
27
|
+
@attributes["sameSite"]
|
28
|
+
end
|
29
|
+
|
26
30
|
def size
|
27
31
|
@attributes["size"]
|
28
32
|
end
|
@@ -65,6 +69,9 @@ module Ferrum
|
|
65
69
|
cookie[:value] ||= value
|
66
70
|
cookie[:domain] ||= default_domain
|
67
71
|
|
72
|
+
cookie[:httpOnly] = cookie.delete(:httponly) if cookie.key?(:httponly)
|
73
|
+
cookie[:sameSite] = cookie.delete(:samesite) if cookie.key?(:samesite)
|
74
|
+
|
68
75
|
expires = cookie.delete(:expires).to_i
|
69
76
|
cookie[:expires] = expires if expires > 0
|
70
77
|
|
data/lib/ferrum/dialog.rb
CHANGED
@@ -14,11 +14,11 @@ module Ferrum
|
|
14
14
|
options = { accept: true }
|
15
15
|
response = prompt_text || default_prompt
|
16
16
|
options.merge!(promptText: response) if response
|
17
|
-
@page.command("Page.handleJavaScriptDialog", **options)
|
17
|
+
@page.command("Page.handleJavaScriptDialog", slowmoable: true, **options)
|
18
18
|
end
|
19
19
|
|
20
20
|
def dismiss
|
21
|
-
@page.command("Page.handleJavaScriptDialog", accept: false)
|
21
|
+
@page.command("Page.handleJavaScriptDialog", slowmoable: true, accept: false)
|
22
22
|
end
|
23
23
|
|
24
24
|
def match?(regexp)
|
data/lib/ferrum/frame.rb
CHANGED
@@ -7,9 +7,8 @@ module Ferrum
|
|
7
7
|
class Frame
|
8
8
|
include DOM, Runtime
|
9
9
|
|
10
|
-
attr_reader :
|
11
|
-
|
12
|
-
attr_accessor :name
|
10
|
+
attr_reader :page, :parent_id, :state
|
11
|
+
attr_accessor :id, :name
|
13
12
|
|
14
13
|
def initialize(id, page, parent_id = nil)
|
15
14
|
@id, @page, @parent_id = id, page, parent_id
|
@@ -18,8 +17,6 @@ module Ferrum
|
|
18
17
|
# Can be one of:
|
19
18
|
# * started_loading
|
20
19
|
# * navigated
|
21
|
-
# * scheduled_navigation
|
22
|
-
# * cleared_scheduled_navigation
|
23
20
|
# * stopped_loading
|
24
21
|
def state=(value)
|
25
22
|
@state = value
|
@@ -37,6 +34,19 @@ module Ferrum
|
|
37
34
|
@parent_id.nil?
|
38
35
|
end
|
39
36
|
|
37
|
+
def set_content(html)
|
38
|
+
evaluate_async(%(
|
39
|
+
document.open();
|
40
|
+
document.write(arguments[0]);
|
41
|
+
document.close();
|
42
|
+
arguments[1](true);
|
43
|
+
), @page.timeout, html)
|
44
|
+
end
|
45
|
+
|
46
|
+
def execution_id?(execution_id)
|
47
|
+
@execution_id == execution_id
|
48
|
+
end
|
49
|
+
|
40
50
|
def execution_id
|
41
51
|
raise NoExecutionContextError unless @execution_id
|
42
52
|
@execution_id
|
@@ -45,6 +55,14 @@ module Ferrum
|
|
45
55
|
@page.event.wait(@page.timeout) ? retry : raise
|
46
56
|
end
|
47
57
|
|
58
|
+
def set_execution_id(value)
|
59
|
+
@execution_id ||= value
|
60
|
+
end
|
61
|
+
|
62
|
+
def reset_execution_id
|
63
|
+
@execution_id = nil
|
64
|
+
end
|
65
|
+
|
48
66
|
def inspect
|
49
67
|
%(#<#{self.class} @id=#{@id.inspect} @parent_id=#{@parent_id.inspect} @name=#{@name.inspect} @state=#{@state.inspect} @execution_id=#{@execution_id.inspect}>)
|
50
68
|
end
|
data/lib/ferrum/frame/dom.rb
CHANGED
@@ -29,65 +29,62 @@ module Ferrum
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def doctype
|
32
|
-
evaluate("new XMLSerializer().serializeToString(document.doctype)")
|
32
|
+
evaluate("document.doctype && new XMLSerializer().serializeToString(document.doctype)")
|
33
33
|
end
|
34
34
|
|
35
35
|
def body
|
36
36
|
evaluate("document.documentElement.outerHTML")
|
37
37
|
end
|
38
38
|
|
39
|
-
def at_xpath(selector, within: nil)
|
40
|
-
xpath(selector, within: within).first
|
41
|
-
end
|
42
|
-
|
43
|
-
# FIXME: Check within
|
44
39
|
def xpath(selector, within: nil)
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
let results = [];
|
40
|
+
code = <<~JS
|
41
|
+
let selector = arguments[0];
|
42
|
+
let within = arguments[1] || document;
|
43
|
+
let results = [];
|
50
44
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
45
|
+
let xpath = document.evaluate(selector, within, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
46
|
+
for (let i = 0; i < xpath.snapshotLength; i++) {
|
47
|
+
results.push(xpath.snapshotItem(i));
|
48
|
+
}
|
55
49
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
throw "Invalid Selector";
|
61
|
-
} else {
|
62
|
-
throw error;
|
63
|
-
}
|
64
|
-
}), @page.timeout, selector, within)
|
50
|
+
arguments[2](results);
|
51
|
+
JS
|
52
|
+
|
53
|
+
evaluate_async(code, @page.timeout, selector, within)
|
65
54
|
end
|
66
55
|
|
67
|
-
|
68
|
-
|
69
|
-
|
56
|
+
def at_xpath(selector, within: nil)
|
57
|
+
code = <<~JS
|
58
|
+
let selector = arguments[0];
|
59
|
+
let within = arguments[1] || document;
|
60
|
+
let xpath = document.evaluate(selector, within, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
61
|
+
let result = xpath.snapshotItem(0);
|
62
|
+
arguments[2](result);
|
63
|
+
JS
|
70
64
|
|
71
|
-
|
72
|
-
nodeId: node_id,
|
73
|
-
selector: selector)["nodeIds"]
|
74
|
-
ids.map { |id| build_node(id) }.compact
|
65
|
+
evaluate_async(code, @page.timeout, selector, within)
|
75
66
|
end
|
76
67
|
|
77
|
-
def
|
78
|
-
|
68
|
+
def css(selector, within: nil)
|
69
|
+
code = <<~JS
|
70
|
+
let selector = arguments[0];
|
71
|
+
let within = arguments[1] || document;
|
72
|
+
let results = within.querySelectorAll(selector);
|
73
|
+
arguments[2](results);
|
74
|
+
JS
|
79
75
|
|
80
|
-
|
81
|
-
nodeId: node_id,
|
82
|
-
selector: selector)["nodeId"]
|
83
|
-
build_node(id)
|
76
|
+
evaluate_async(code, @page.timeout, selector, within)
|
84
77
|
end
|
85
78
|
|
86
|
-
|
79
|
+
def at_css(selector, within: nil)
|
80
|
+
code = <<~JS
|
81
|
+
let selector = arguments[0];
|
82
|
+
let within = arguments[1] || document;
|
83
|
+
let result = within.querySelector(selector);
|
84
|
+
arguments[2](result);
|
85
|
+
JS
|
87
86
|
|
88
|
-
|
89
|
-
description = @page.command("DOM.describeNode", nodeId: node_id)
|
90
|
-
Node.new(self, @page.target_id, node_id, description["node"])
|
87
|
+
evaluate_async(code, @page.timeout, selector, within)
|
91
88
|
end
|
92
89
|
end
|
93
90
|
end
|
data/lib/ferrum/frame/runtime.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "singleton"
|
4
|
+
|
3
5
|
module Ferrum
|
4
6
|
class Frame
|
5
7
|
module Runtime
|
@@ -85,8 +87,10 @@ module Ferrum
|
|
85
87
|
options.merge!(returnByValue: by_value)
|
86
88
|
|
87
89
|
response = @page.command("Runtime.callFunctionOn",
|
88
|
-
|
89
|
-
|
90
|
+
wait: wait, slowmoable: true,
|
91
|
+
**options)
|
92
|
+
handle_error(response)
|
93
|
+
response = response["result"]
|
90
94
|
|
91
95
|
by_value ? response.dig("value") : handle_response(response)
|
92
96
|
end
|
@@ -137,14 +141,18 @@ module Ferrum
|
|
137
141
|
end
|
138
142
|
|
139
143
|
response = @page.command("Runtime.callFunctionOn",
|
140
|
-
|
144
|
+
slowmoable: true,
|
145
|
+
**params)
|
146
|
+
handle_error(response)
|
147
|
+
response = response["result"]
|
141
148
|
|
142
149
|
handle ? handle_response(response) : response
|
143
150
|
end
|
144
151
|
end
|
145
152
|
|
146
153
|
# FIXME: We should have a central place to handle all type of errors
|
147
|
-
def handle_error(
|
154
|
+
def handle_error(response)
|
155
|
+
result = response["result"]
|
148
156
|
return if result["subtype"] != "error"
|
149
157
|
|
150
158
|
case result["description"]
|
@@ -209,7 +217,7 @@ module Ferrum
|
|
209
217
|
|
210
218
|
def reduce_props(object_id, to)
|
211
219
|
if cyclic?(object_id).dig("result", "value")
|
212
|
-
return
|
220
|
+
return to.is_a?(Array) ? [cyclic_object] : cyclic_object
|
213
221
|
else
|
214
222
|
props = @page.command("Runtime.getProperties", ownProperties: true, objectId: object_id)
|
215
223
|
props["result"].reduce(to) do |memo, prop|
|
@@ -220,38 +228,51 @@ module Ferrum
|
|
220
228
|
end
|
221
229
|
|
222
230
|
def cyclic?(object_id)
|
223
|
-
@page.command(
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
231
|
+
@page.command(
|
232
|
+
"Runtime.callFunctionOn",
|
233
|
+
objectId: object_id,
|
234
|
+
returnByValue: true,
|
235
|
+
functionDeclaration: <<~JS
|
236
|
+
function() {
|
237
|
+
if (Array.isArray(this) &&
|
238
|
+
this.every(e => e instanceof Node)) {
|
239
|
+
return false;
|
240
|
+
}
|
232
241
|
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
}
|
244
|
-
}
|
245
|
-
}
|
246
|
-
|
247
|
-
return false;
|
242
|
+
const seen = [];
|
243
|
+
function detectCycle(obj) {
|
244
|
+
if (typeof obj === "object") {
|
245
|
+
if (seen.indexOf(obj) !== -1) {
|
246
|
+
return true;
|
247
|
+
}
|
248
|
+
seen.push(obj);
|
249
|
+
for (let key in obj) {
|
250
|
+
if (obj.hasOwnProperty(key) && detectCycle(obj[key])) {
|
251
|
+
return true;
|
248
252
|
}
|
249
|
-
|
250
|
-
return detectCycle(this);
|
251
253
|
}
|
252
|
-
|
253
|
-
|
254
|
+
}
|
255
|
+
|
256
|
+
return false;
|
257
|
+
}
|
258
|
+
|
259
|
+
return detectCycle(this);
|
260
|
+
}
|
261
|
+
JS
|
262
|
+
)
|
254
263
|
end
|
264
|
+
|
265
|
+
def cyclic_object
|
266
|
+
CyclicObject.instance
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
class CyclicObject
|
272
|
+
include Singleton
|
273
|
+
|
274
|
+
def inspect
|
275
|
+
%(#<#{self.class} JavaScript object that cannot be represented in Ruby>)
|
255
276
|
end
|
256
277
|
end
|
257
278
|
end
|
data/lib/ferrum/headers.rb
CHANGED
@@ -40,7 +40,7 @@ module Ferrum
|
|
40
40
|
|
41
41
|
def set_overrides(user_agent: nil, accept_language: nil, platform: nil)
|
42
42
|
options = Hash.new
|
43
|
-
options[:userAgent] = user_agent
|
43
|
+
options[:userAgent] = user_agent || @page.browser.default_user_agent
|
44
44
|
options[:acceptLanguage] = accept_language if accept_language
|
45
45
|
options[:platform] if platform
|
46
46
|
|