ferrum 0.1.2 → 0.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/README.md +513 -10
- data/lib/ferrum.rb +85 -4
- data/lib/ferrum/browser.rb +16 -33
- data/lib/ferrum/browser/client.rb +18 -2
- data/lib/ferrum/browser/subscriber.rb +5 -1
- data/lib/ferrum/cookies.rb +97 -0
- data/lib/ferrum/headers.rb +50 -0
- data/lib/ferrum/{page/input.json → keyboard.json} +0 -0
- data/lib/ferrum/keyboard.rb +119 -0
- data/lib/ferrum/mouse.rb +53 -0
- data/lib/ferrum/network/intercepted_request.rb +53 -0
- data/lib/ferrum/node.rb +52 -115
- data/lib/ferrum/page.rb +30 -30
- data/lib/ferrum/page/dom.rb +22 -12
- data/lib/ferrum/page/frame.rb +13 -12
- data/lib/ferrum/page/input.rb +3 -148
- data/lib/ferrum/page/net.rb +66 -54
- data/lib/ferrum/page/runtime.rb +13 -19
- data/lib/ferrum/page/screenshot.rb +84 -0
- data/lib/ferrum/targets.rb +4 -8
- data/lib/ferrum/version.rb +1 -1
- metadata +9 -10
- data/lib/ferrum/browser/api.rb +0 -14
- data/lib/ferrum/browser/api/cookie.rb +0 -46
- data/lib/ferrum/browser/api/header.rb +0 -32
- data/lib/ferrum/browser/api/intercept.rb +0 -32
- data/lib/ferrum/browser/api/screenshot.rb +0 -78
- data/lib/ferrum/cookie.rb +0 -47
- data/lib/ferrum/errors.rb +0 -94
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ferrum
|
4
|
+
class Page
|
5
|
+
module Screenshot
|
6
|
+
def screenshot(**opts)
|
7
|
+
path, encoding = common_options(**opts)
|
8
|
+
options = screenshot_options(path, **opts)
|
9
|
+
data = command("Page.captureScreenshot", **options).fetch("data")
|
10
|
+
return data if encoding == :base64
|
11
|
+
save_file(path, data)
|
12
|
+
end
|
13
|
+
|
14
|
+
def pdf(**opts)
|
15
|
+
path, encoding = common_options(**opts)
|
16
|
+
options = pdf_options(**opts)
|
17
|
+
data = command("Page.printToPDF", **options).fetch("data")
|
18
|
+
return data if encoding == :base64
|
19
|
+
save_file(path, data)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def save_file(path, data)
|
25
|
+
bin = Base64.decode64(data)
|
26
|
+
File.open(path.to_s, "wb") { |f| f.write(bin) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def common_options(encoding: :base64, path: nil, **_)
|
30
|
+
encoding = encoding.to_sym
|
31
|
+
encoding = :binary if path
|
32
|
+
|
33
|
+
if encoding == :binary && !path
|
34
|
+
raise "You have to provide `:path` for `:binary` encoding"
|
35
|
+
end
|
36
|
+
|
37
|
+
[path, encoding]
|
38
|
+
end
|
39
|
+
|
40
|
+
def pdf_options(landscape: false, paper_width: 8.5, paper_height: 11, scale: 1.0, **opts)
|
41
|
+
options = {}
|
42
|
+
options[:landscape] = landscape
|
43
|
+
options[:paperWidth] = paper_width.to_f
|
44
|
+
options[:paperHeight] = paper_height.to_f
|
45
|
+
options[:scale] = scale.to_f
|
46
|
+
options.merge(opts)
|
47
|
+
end
|
48
|
+
|
49
|
+
def screenshot_options(path = nil, format: nil, scale: 1.0, **opts)
|
50
|
+
options = {}
|
51
|
+
|
52
|
+
format ||= path ? File.extname(path).delete(".") : "png"
|
53
|
+
format = "jpeg" if format == "jpg"
|
54
|
+
raise "Not supported options `:format` #{format}. jpeg | png" if format !~ /jpeg|png/i
|
55
|
+
options.merge!(format: format)
|
56
|
+
|
57
|
+
options.merge!(quality: opts[:quality] ? opts[:quality] : 75) if format == "jpeg"
|
58
|
+
|
59
|
+
if !!opts[:full] && opts[:selector]
|
60
|
+
warn "Ignoring :selector in #screenshot since full: true was given at #{caller(1..1).first}"
|
61
|
+
end
|
62
|
+
|
63
|
+
if !!opts[:full]
|
64
|
+
width, height = evaluate("[document.documentElement.offsetWidth, document.documentElement.offsetHeight]")
|
65
|
+
options.merge!(clip: { x: 0, y: 0, width: width, height: height, scale: scale }) if width > 0 && height > 0
|
66
|
+
elsif opts[:selector]
|
67
|
+
rect = evaluate("document.querySelector('#{opts[:selector]}').getBoundingClientRect()")
|
68
|
+
options.merge!(clip: { x: rect["x"], y: rect["y"], width: rect["width"], height: rect["height"], scale: scale })
|
69
|
+
end
|
70
|
+
|
71
|
+
if scale != 1.0
|
72
|
+
if !options[:clip]
|
73
|
+
width, height = evaluate("[document.documentElement.clientWidth, document.documentElement.clientHeight]")
|
74
|
+
options[:clip] = { x: 0, y: 0, width: width, height: height }
|
75
|
+
end
|
76
|
+
|
77
|
+
options[:clip].merge!(scale: scale)
|
78
|
+
end
|
79
|
+
|
80
|
+
options
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/lib/ferrum/targets.rb
CHANGED
@@ -102,17 +102,13 @@ module Ferrum
|
|
102
102
|
end
|
103
103
|
|
104
104
|
def targets
|
105
|
-
|
106
|
-
|
105
|
+
Ferrum.with_attempts(errors: EmptyTargetsError,
|
106
|
+
max: TARGETS_RETRY_ATTEMPTS,
|
107
|
+
wait: TARGETS_RETRY_WAIT) do
|
107
108
|
# Targets cannot be empty the must be at least one default target.
|
108
109
|
targets = @browser.command("Target.getTargets")["targetInfos"]
|
109
|
-
raise EmptyTargetsError
|
110
|
+
raise EmptyTargetsError if targets.empty?
|
110
111
|
targets
|
111
|
-
rescue EmptyTargetsError
|
112
|
-
raise if attempts > TARGETS_RETRY_ATTEMPTS
|
113
|
-
attempts += 1
|
114
|
-
sleep TARGETS_RETRY_WAIT
|
115
|
-
retry
|
116
112
|
end
|
117
113
|
end
|
118
114
|
|
data/lib/ferrum/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ferrum
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: '0.2'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dmitry Vorotilin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-09-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: websocket-driver
|
@@ -195,28 +195,27 @@ files:
|
|
195
195
|
- README.md
|
196
196
|
- lib/ferrum.rb
|
197
197
|
- lib/ferrum/browser.rb
|
198
|
-
- lib/ferrum/browser/api.rb
|
199
|
-
- lib/ferrum/browser/api/cookie.rb
|
200
|
-
- lib/ferrum/browser/api/header.rb
|
201
|
-
- lib/ferrum/browser/api/intercept.rb
|
202
|
-
- lib/ferrum/browser/api/screenshot.rb
|
203
198
|
- lib/ferrum/browser/client.rb
|
204
199
|
- lib/ferrum/browser/process.rb
|
205
200
|
- lib/ferrum/browser/subscriber.rb
|
206
201
|
- lib/ferrum/browser/web_socket.rb
|
207
|
-
- lib/ferrum/
|
208
|
-
- lib/ferrum/
|
202
|
+
- lib/ferrum/cookies.rb
|
203
|
+
- lib/ferrum/headers.rb
|
204
|
+
- lib/ferrum/keyboard.json
|
205
|
+
- lib/ferrum/keyboard.rb
|
206
|
+
- lib/ferrum/mouse.rb
|
209
207
|
- lib/ferrum/network/error.rb
|
208
|
+
- lib/ferrum/network/intercepted_request.rb
|
210
209
|
- lib/ferrum/network/request.rb
|
211
210
|
- lib/ferrum/network/response.rb
|
212
211
|
- lib/ferrum/node.rb
|
213
212
|
- lib/ferrum/page.rb
|
214
213
|
- lib/ferrum/page/dom.rb
|
215
214
|
- lib/ferrum/page/frame.rb
|
216
|
-
- lib/ferrum/page/input.json
|
217
215
|
- lib/ferrum/page/input.rb
|
218
216
|
- lib/ferrum/page/net.rb
|
219
217
|
- lib/ferrum/page/runtime.rb
|
218
|
+
- lib/ferrum/page/screenshot.rb
|
220
219
|
- lib/ferrum/targets.rb
|
221
220
|
- lib/ferrum/version.rb
|
222
221
|
homepage: https://github.com/route/ferrum
|
data/lib/ferrum/browser/api.rb
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "ferrum/browser/api/cookie"
|
4
|
-
require "ferrum/browser/api/header"
|
5
|
-
require "ferrum/browser/api/screenshot"
|
6
|
-
require "ferrum/browser/api/intercept"
|
7
|
-
|
8
|
-
module Ferrum
|
9
|
-
class Browser
|
10
|
-
module API
|
11
|
-
include Cookie, Header, Screenshot, Intercept
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
@@ -1,46 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Ferrum
|
4
|
-
class Browser
|
5
|
-
module API
|
6
|
-
module Cookie
|
7
|
-
def cookies
|
8
|
-
cookies = page.command("Network.getAllCookies")["cookies"]
|
9
|
-
cookies.map { |c| [c["name"], ::Ferrum::Cookie.new(c)] }.to_h
|
10
|
-
end
|
11
|
-
|
12
|
-
def set_cookie(name: nil, value: nil, cookie: nil, **options)
|
13
|
-
cookie = options.dup
|
14
|
-
cookie[:name] ||= name
|
15
|
-
cookie[:value] ||= value
|
16
|
-
cookie[:domain] ||= default_domain
|
17
|
-
|
18
|
-
expires = cookie.delete(:expires).to_i
|
19
|
-
cookie[:expires] = expires if expires > 0
|
20
|
-
|
21
|
-
page.command("Network.setCookie", **cookie)
|
22
|
-
end
|
23
|
-
|
24
|
-
# Supports :url, :domain and :path options
|
25
|
-
def remove_cookie(name:, **options)
|
26
|
-
raise "Specify :domain or :url option" if !options[:domain] && !options[:url] && !default_domain
|
27
|
-
|
28
|
-
options = options.merge(name: name)
|
29
|
-
options[:domain] ||= default_domain
|
30
|
-
|
31
|
-
page.command("Network.deleteCookies", **options)
|
32
|
-
end
|
33
|
-
|
34
|
-
def clear_cookies
|
35
|
-
page.command("Network.clearBrowserCookies")
|
36
|
-
end
|
37
|
-
|
38
|
-
private
|
39
|
-
|
40
|
-
def default_domain
|
41
|
-
URI.parse(base_url).host if base_url
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
@@ -1,32 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Ferrum
|
4
|
-
class Browser
|
5
|
-
module API
|
6
|
-
module Header
|
7
|
-
def headers=(headers)
|
8
|
-
@headers = {}
|
9
|
-
add_headers(headers)
|
10
|
-
end
|
11
|
-
|
12
|
-
def add_headers(headers, permanent: true)
|
13
|
-
if headers["Referer"]
|
14
|
-
page.referrer = headers["Referer"]
|
15
|
-
headers.delete("Referer") unless permanent
|
16
|
-
end
|
17
|
-
|
18
|
-
@headers.merge!(headers)
|
19
|
-
user_agent = @headers["User-Agent"]
|
20
|
-
accept_language = @headers["Accept-Language"]
|
21
|
-
|
22
|
-
set_overrides(user_agent: user_agent, accept_language: accept_language)
|
23
|
-
page.command("Network.setExtraHTTPHeaders", headers: @headers)
|
24
|
-
end
|
25
|
-
|
26
|
-
def add_header(header, permanent: true)
|
27
|
-
add_headers(header, permanent: permanent)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
@@ -1,32 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Ferrum
|
4
|
-
class Browser
|
5
|
-
module API
|
6
|
-
module Intercept
|
7
|
-
def url_whitelist=(wildcards)
|
8
|
-
@url_whitelist = prepare_wildcards(wildcards)
|
9
|
-
page.intercept_request("*") if @client && !@url_whitelist.empty?
|
10
|
-
end
|
11
|
-
|
12
|
-
def url_blacklist=(wildcards)
|
13
|
-
@url_blacklist = prepare_wildcards(wildcards)
|
14
|
-
page.intercept_request("*") if @client && !@url_blacklist.empty?
|
15
|
-
end
|
16
|
-
|
17
|
-
private
|
18
|
-
|
19
|
-
def prepare_wildcards(wc)
|
20
|
-
Array(wc).map do |wildcard|
|
21
|
-
if wildcard.is_a?(Regexp)
|
22
|
-
wildcard
|
23
|
-
else
|
24
|
-
wildcard = wildcard.gsub("*", ".*")
|
25
|
-
Regexp.new(wildcard, Regexp::IGNORECASE)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
@@ -1,78 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Ferrum
|
4
|
-
class Browser
|
5
|
-
module API
|
6
|
-
module Screenshot
|
7
|
-
def screenshot(**opts)
|
8
|
-
encoding, path, options = screenshot_options(**opts)
|
9
|
-
|
10
|
-
data = if options[:format].to_s == "pdf"
|
11
|
-
options = {}
|
12
|
-
options[:paperWidth] = @paper_size[:width].to_f if @paper_size
|
13
|
-
options[:paperHeight] = @paper_size[:height].to_f if @paper_size
|
14
|
-
options[:scale] = @zoom_factor if @zoom_factor
|
15
|
-
page.command("Page.printToPDF", **options)
|
16
|
-
else
|
17
|
-
page.command("Page.captureScreenshot", **options)
|
18
|
-
end.fetch("data")
|
19
|
-
|
20
|
-
return data if encoding == :base64
|
21
|
-
|
22
|
-
bin = Base64.decode64(data)
|
23
|
-
File.open(path.to_s, "wb") { |f| f.write(bin) }
|
24
|
-
end
|
25
|
-
|
26
|
-
def zoom_factor=(value)
|
27
|
-
@zoom_factor = value.to_f
|
28
|
-
end
|
29
|
-
|
30
|
-
def paper_size=(value)
|
31
|
-
@paper_size = value
|
32
|
-
end
|
33
|
-
|
34
|
-
private
|
35
|
-
|
36
|
-
def screenshot_options(encoding: :base64, format: nil, path: nil, **opts)
|
37
|
-
options = {}
|
38
|
-
|
39
|
-
encoding = :binary if path
|
40
|
-
|
41
|
-
if encoding == :binary && !path
|
42
|
-
raise "Not supported option `:path` #{path}. Should be path to file"
|
43
|
-
end
|
44
|
-
|
45
|
-
format ||= path ? File.extname(path).delete(".") : "png"
|
46
|
-
format = "jpeg" if format == "jpg"
|
47
|
-
raise "Not supported options `:format` #{format}. jpeg | png | pdf" if format !~ /jpeg|png|pdf/i
|
48
|
-
options.merge!(format: format)
|
49
|
-
|
50
|
-
options.merge!(quality: opts[:quality] ? opts[:quality] : 75) if format == "jpeg"
|
51
|
-
|
52
|
-
if !!opts[:full] && opts[:selector]
|
53
|
-
warn "Ignoring :selector in #screenshot since full: true was given at #{caller(1..1).first}"
|
54
|
-
end
|
55
|
-
|
56
|
-
if !!opts[:full]
|
57
|
-
width, height = page.evaluate("[document.documentElement.offsetWidth, document.documentElement.offsetHeight]")
|
58
|
-
options.merge!(clip: { x: 0, y: 0, width: width, height: height, scale: @zoom_factor || 1.0 }) if width > 0 && height > 0
|
59
|
-
elsif opts[:selector]
|
60
|
-
rect = page.evaluate("document.querySelector('#{opts[:selector]}').getBoundingClientRect()")
|
61
|
-
options.merge!(clip: { x: rect["x"], y: rect["y"], width: rect["width"], height: rect["height"], scale: @zoom_factor || 1.0 })
|
62
|
-
end
|
63
|
-
|
64
|
-
if @zoom_factor
|
65
|
-
if !options[:clip]
|
66
|
-
width, height = page.evaluate("[document.documentElement.clientWidth, document.documentElement.clientHeight]")
|
67
|
-
options[:clip] = { x: 0, y: 0, width: width, height: height }
|
68
|
-
end
|
69
|
-
|
70
|
-
options[:clip].merge!(scale: @zoom_factor)
|
71
|
-
end
|
72
|
-
|
73
|
-
[encoding, path, options]
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
data/lib/ferrum/cookie.rb
DELETED
@@ -1,47 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Ferrum
|
4
|
-
class Cookie
|
5
|
-
def initialize(attributes)
|
6
|
-
@attributes = attributes
|
7
|
-
end
|
8
|
-
|
9
|
-
def name
|
10
|
-
@attributes["name"]
|
11
|
-
end
|
12
|
-
|
13
|
-
def value
|
14
|
-
@attributes["value"]
|
15
|
-
end
|
16
|
-
|
17
|
-
def domain
|
18
|
-
@attributes["domain"]
|
19
|
-
end
|
20
|
-
|
21
|
-
def path
|
22
|
-
@attributes["path"]
|
23
|
-
end
|
24
|
-
|
25
|
-
def size
|
26
|
-
@attributes["size"]
|
27
|
-
end
|
28
|
-
|
29
|
-
def secure?
|
30
|
-
@attributes["secure"]
|
31
|
-
end
|
32
|
-
|
33
|
-
def httponly?
|
34
|
-
@attributes["httpOnly"]
|
35
|
-
end
|
36
|
-
|
37
|
-
def session?
|
38
|
-
@attributes["session"]
|
39
|
-
end
|
40
|
-
|
41
|
-
def expires
|
42
|
-
if @attributes["expires"] > 0
|
43
|
-
Time.at(@attributes["expires"])
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
data/lib/ferrum/errors.rb
DELETED
@@ -1,94 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Ferrum
|
4
|
-
class NotImplemented < StandardError; end
|
5
|
-
class ModalNotFound < StandardError; end
|
6
|
-
class Error < StandardError; end
|
7
|
-
class NoSuchWindowError < Error; end
|
8
|
-
class EmptyTargetsError < Error; end
|
9
|
-
class NoExecutionContext < Error; end
|
10
|
-
|
11
|
-
class ClientError < Error
|
12
|
-
attr_reader :response
|
13
|
-
|
14
|
-
def initialize(response)
|
15
|
-
@response = response
|
16
|
-
super(response["message"])
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
class BrowserError < ClientError
|
21
|
-
def code
|
22
|
-
response["code"]
|
23
|
-
end
|
24
|
-
|
25
|
-
def data
|
26
|
-
response["data"]
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
class JavaScriptError < ClientError
|
31
|
-
attr_reader :class_name, :message
|
32
|
-
|
33
|
-
def initialize(response)
|
34
|
-
super
|
35
|
-
@class_name, @message = response.values_at("className", "description")
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
class StatusFailError < ClientError
|
40
|
-
def message
|
41
|
-
"Request to #{response["url"]} failed to reach server, check DNS and/or server status"
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
class FrameNotFound < ClientError
|
46
|
-
def name
|
47
|
-
response["args"].first
|
48
|
-
end
|
49
|
-
|
50
|
-
def message
|
51
|
-
"The frame \"#{name}\" was not found."
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
class NodeError < ClientError
|
56
|
-
attr_reader :node
|
57
|
-
|
58
|
-
def initialize(node, response)
|
59
|
-
@node = node
|
60
|
-
super(response)
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
class ObsoleteNode < NodeError
|
65
|
-
def message
|
66
|
-
"The element you are trying to interact with is either not part of the DOM, or is " \
|
67
|
-
"not currently visible on the page (perhaps display: none is set). " \
|
68
|
-
"It is possible the element has been replaced by another element and you meant to interact with " \
|
69
|
-
"the new element. If so you need to do a new find in order to get a reference to the " \
|
70
|
-
"new element."
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
class TimeoutError < Error
|
75
|
-
def message
|
76
|
-
"Timed out waiting for response. It's possible that this happened " \
|
77
|
-
"because something took a very long time (for example a page load " \
|
78
|
-
"was slow). If so, setting the :timeout option to a higher value might " \
|
79
|
-
"help."
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
class ScriptTimeoutError < Error
|
84
|
-
def message
|
85
|
-
"Timed out waiting for evaluated script to return a value"
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
class DeadBrowser < Error
|
90
|
-
def initialize(message = "Browser is dead")
|
91
|
-
super
|
92
|
-
end
|
93
|
-
end
|
94
|
-
end
|