ferrum 0.8 → 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 +6 -1
- data/lib/ferrum.rb +13 -0
- data/lib/ferrum/browser.rb +2 -1
- data/lib/ferrum/browser/command.rb +25 -24
- 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 +18 -8
- data/lib/ferrum/browser/subscriber.rb +1 -1
- data/lib/ferrum/browser/web_socket.rb +17 -1
- data/lib/ferrum/browser/xvfb.rb +37 -0
- data/lib/ferrum/frame/runtime.rb +15 -1
- data/lib/ferrum/network/intercepted_request.rb +1 -1
- data/lib/ferrum/node.rb +38 -17
- data/lib/ferrum/page.rb +7 -1
- data/lib/ferrum/version.rb +1 -1
- metadata +8 -6
- data/lib/ferrum/browser/chrome.rb +0 -76
- data/lib/ferrum/browser/firefox.rb +0 -34
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8b4d6dc7aa1827fbf559e6025b82d29d15ed0e36a89793049266d8049fadabb9
|
4
|
+
data.tar.gz: 6fab0202e85a17971d613db12e37a7ef85325eaf23f718b6801812df565ac64c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fb109c1b65e73e8d0088734fa004fae3d15650121d01b44dc9086cdb193bb3c7f56c3ad911a2f6de5cd28522231b4e695b67eb515d90afe93b862eb64cfe7050
|
7
|
+
data.tar.gz: a4d5e4c192cbd634c640d86027f3a8faeaf42efcd8fbaa9b0e8c6e81c04aa9921b7f2353eb739c35850363f1ed58f84d5895714f52ecd333d9ebdb50c1de4031
|
data/README.md
CHANGED
@@ -90,7 +90,7 @@ Interact with a page:
|
|
90
90
|
```ruby
|
91
91
|
browser = Ferrum::Browser.new
|
92
92
|
browser.goto("https://google.com")
|
93
|
-
input = browser.at_xpath("//
|
93
|
+
input = browser.at_xpath("//input[@name='q']")
|
94
94
|
input.focus.type("Ruby headless driver for Chrome", :Enter)
|
95
95
|
browser.at_css("a > h3").text # => "rubycdp/ferrum: Ruby Chrome/Chromium driver - GitHub"
|
96
96
|
browser.quit
|
@@ -147,6 +147,7 @@ Ferrum::Browser.new(options)
|
|
147
147
|
|
148
148
|
* options `Hash`
|
149
149
|
* `:headless` (Boolean) - Set browser as headless or not, `true` by default.
|
150
|
+
* `:xvfb` (Boolean) - Run browser in a virtual framebuffer, `false` by default.
|
150
151
|
* `:window_size` (Array) - The dimensions of the browser window in which to
|
151
152
|
test, expressed as a 2-element array, e.g. [1024, 768]. Default: [1024, 768]
|
152
153
|
* `:extensions` (Array[String | Hash]) - An array of paths to files or JS
|
@@ -166,6 +167,10 @@ Ferrum::Browser.new(options)
|
|
166
167
|
* `:browser_options` (Hash) - Additional command line options,
|
167
168
|
[see them all](https://peter.sh/experiments/chromium-command-line-switches/)
|
168
169
|
e.g. `{ "ignore-certificate-errors" => nil }`
|
170
|
+
* `:ignore_default_browser_options` (Boolean) - Ferrum has a number of default
|
171
|
+
options it passes to the browser, if you set this to `true` then only
|
172
|
+
options you put in `:browser_options` will be passed to the browser,
|
173
|
+
except required ones of course.
|
169
174
|
* `:port` (Integer) - Remote debugging port for headless Chrome
|
170
175
|
* `:host` (String) - Remote debugging address for headless Chrome
|
171
176
|
* `:url` (String) - URL for a running instance of Chrome. If this is set, a
|
data/lib/ferrum.rb
CHANGED
@@ -48,6 +48,19 @@ module Ferrum
|
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
51
|
+
class NodeIsMovingError < Error
|
52
|
+
def initialize(node, prev, current)
|
53
|
+
@node, @prev, @current = node, prev, current
|
54
|
+
super(message)
|
55
|
+
end
|
56
|
+
|
57
|
+
def message
|
58
|
+
"#{@node.inspect} that you're trying to click is moving, hence " \
|
59
|
+
"we cannot. Previosuly it was at #{@prev.inspect} but now at " \
|
60
|
+
"#{@current.inspect}."
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
51
64
|
class BrowserError < Error
|
52
65
|
attr_reader :response
|
53
66
|
|
data/lib/ferrum/browser.rb
CHANGED
@@ -4,6 +4,7 @@ require "base64"
|
|
4
4
|
require "forwardable"
|
5
5
|
require "ferrum/page"
|
6
6
|
require "ferrum/contexts"
|
7
|
+
require "ferrum/browser/xvfb"
|
7
8
|
require "ferrum/browser/process"
|
8
9
|
require "ferrum/browser/client"
|
9
10
|
|
@@ -16,7 +17,7 @@ module Ferrum
|
|
16
17
|
extend Forwardable
|
17
18
|
delegate %i[default_context] => :contexts
|
18
19
|
delegate %i[targets create_target create_page page pages windows] => :default_context
|
19
|
-
delegate %i[goto back forward refresh reload stop
|
20
|
+
delegate %i[goto back forward refresh reload stop wait_for_reload
|
20
21
|
at_css at_xpath css xpath current_url current_title url title
|
21
22
|
body doctype set_content
|
22
23
|
headers cookies network
|
@@ -3,8 +3,6 @@
|
|
3
3
|
module Ferrum
|
4
4
|
class Browser
|
5
5
|
class Command
|
6
|
-
BROWSER_HOST = "127.0.0.1"
|
7
|
-
BROWSER_PORT = "0"
|
8
6
|
NOT_FOUND = "Could not find an executable for the browser. Try to make " \
|
9
7
|
"it available on the PATH or set environment varible for " \
|
10
8
|
"example BROWSER_PATH=\"/usr/bin/chrome\"".freeze
|
@@ -12,44 +10,47 @@ module Ferrum
|
|
12
10
|
# Currently only these browsers support CDP:
|
13
11
|
# https://github.com/cyrus-and/chrome-remote-interface#implementations
|
14
12
|
def self.build(options, user_data_dir)
|
15
|
-
case options[:browser_name]
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
13
|
+
defaults = case options[:browser_name]
|
14
|
+
when :firefox
|
15
|
+
Options::Firefox.options
|
16
|
+
when :chrome, :opera, :edge, nil
|
17
|
+
Options::Chrome.options
|
18
|
+
else
|
19
|
+
raise NotImplementedError, "not supported browser"
|
20
|
+
end
|
21
|
+
|
22
|
+
new(defaults, options, user_data_dir)
|
23
23
|
end
|
24
24
|
|
25
|
-
attr_reader :
|
25
|
+
attr_reader :defaults, :path, :options
|
26
26
|
|
27
|
-
def initialize(options, user_data_dir)
|
27
|
+
def initialize(defaults, options, user_data_dir)
|
28
28
|
@flags = {}
|
29
|
+
@defaults = defaults
|
29
30
|
@options, @user_data_dir = options, user_data_dir
|
30
|
-
@path = options[:browser_path] || ENV["BROWSER_PATH"] || detect_path
|
31
|
+
@path = options[:browser_path] || ENV["BROWSER_PATH"] || defaults.detect_path
|
31
32
|
raise Cliver::Dependency::NotFound.new(NOT_FOUND) unless @path
|
33
|
+
merge_options
|
34
|
+
end
|
32
35
|
|
33
|
-
|
36
|
+
def xvfb?
|
37
|
+
!!options[:xvfb]
|
34
38
|
end
|
35
39
|
|
36
40
|
def to_a
|
37
|
-
[path] + flags.map { |k, v| v.nil? ? "--#{k}" : "--#{k}=#{v}" }
|
41
|
+
[path] + @flags.map { |k, v| v.nil? ? "--#{k}" : "--#{k}=#{v}" }
|
38
42
|
end
|
39
43
|
|
40
44
|
private
|
41
45
|
|
42
|
-
def
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
.find { |b| p = Cliver.detect(b) and break(p) }
|
46
|
+
def merge_options
|
47
|
+
@flags = defaults.merge_required(@flags, options, @user_data_dir)
|
48
|
+
|
49
|
+
unless options[:ignore_default_browser_options]
|
50
|
+
@flags = defaults.merge_default(@flags, options)
|
48
51
|
end
|
49
|
-
end
|
50
52
|
|
51
|
-
|
52
|
-
raise NotImplementedError
|
53
|
+
@flags.merge!(options.fetch(:browser_options, {}))
|
53
54
|
end
|
54
55
|
end
|
55
56
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "singleton"
|
4
|
+
|
5
|
+
module Ferrum
|
6
|
+
class Browser
|
7
|
+
module Options
|
8
|
+
class Base
|
9
|
+
BROWSER_HOST = "127.0.0.1"
|
10
|
+
BROWSER_PORT = "0"
|
11
|
+
|
12
|
+
include Singleton
|
13
|
+
|
14
|
+
def self.options
|
15
|
+
instance
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_h
|
19
|
+
self.class::DEFAULT_OPTIONS
|
20
|
+
end
|
21
|
+
|
22
|
+
def except(*keys)
|
23
|
+
to_h.reject { |n, _| keys.include?(n) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def detect_path
|
27
|
+
if Ferrum.mac?
|
28
|
+
self.class::MAC_BIN_PATH.find { |n| File.exist?(n) }
|
29
|
+
else
|
30
|
+
self.class::LINUX_BIN_PATH.find do |name|
|
31
|
+
path = Cliver.detect(name) and break(path)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def merge_required(flags, options, user_data_dir)
|
37
|
+
raise NotImplementedError
|
38
|
+
end
|
39
|
+
|
40
|
+
def merge_default(flags, options)
|
41
|
+
raise NotImplementedError
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ferrum
|
4
|
+
class Browser
|
5
|
+
module Options
|
6
|
+
class Chrome < Base
|
7
|
+
DEFAULT_OPTIONS = {
|
8
|
+
"headless" => nil,
|
9
|
+
"disable-gpu" => nil,
|
10
|
+
"hide-scrollbars" => nil,
|
11
|
+
"mute-audio" => nil,
|
12
|
+
"enable-automation" => nil,
|
13
|
+
"disable-web-security" => nil,
|
14
|
+
"disable-session-crashed-bubble" => nil,
|
15
|
+
"disable-breakpad" => nil,
|
16
|
+
"disable-sync" => nil,
|
17
|
+
"no-first-run" => nil,
|
18
|
+
"use-mock-keychain" => nil,
|
19
|
+
"keep-alive-for-test" => nil,
|
20
|
+
"disable-popup-blocking" => nil,
|
21
|
+
"disable-extensions" => nil,
|
22
|
+
"disable-hang-monitor" => nil,
|
23
|
+
"disable-features" => "site-per-process,TranslateUI",
|
24
|
+
"disable-translate" => nil,
|
25
|
+
"disable-background-networking" => nil,
|
26
|
+
"enable-features" => "NetworkService,NetworkServiceInProcess",
|
27
|
+
"disable-background-timer-throttling" => nil,
|
28
|
+
"disable-backgrounding-occluded-windows" => nil,
|
29
|
+
"disable-client-side-phishing-detection" => nil,
|
30
|
+
"disable-default-apps" => nil,
|
31
|
+
"disable-dev-shm-usage" => nil,
|
32
|
+
"disable-ipc-flooding-protection" => nil,
|
33
|
+
"disable-prompt-on-repost" => nil,
|
34
|
+
"disable-renderer-backgrounding" => nil,
|
35
|
+
"force-color-profile" => "srgb",
|
36
|
+
"metrics-recording-only" => nil,
|
37
|
+
"safebrowsing-disable-auto-update" => nil,
|
38
|
+
"password-store" => "basic",
|
39
|
+
# Note: --no-sandbox is not needed if you properly setup a user in the container.
|
40
|
+
# https://github.com/ebidel/lighthouse-ci/blob/master/builder/Dockerfile#L35-L40
|
41
|
+
# "no-sandbox" => nil,
|
42
|
+
}.freeze
|
43
|
+
|
44
|
+
MAC_BIN_PATH = [
|
45
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
46
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
47
|
+
].freeze
|
48
|
+
LINUX_BIN_PATH = %w[chromium google-chrome-unstable google-chrome-beta
|
49
|
+
google-chrome chrome chromium-browser
|
50
|
+
google-chrome-stable].freeze
|
51
|
+
|
52
|
+
def merge_required(flags, options, user_data_dir)
|
53
|
+
port = options.fetch(:port, BROWSER_PORT)
|
54
|
+
host = options.fetch(:host, BROWSER_HOST)
|
55
|
+
flags.merge("remote-debugging-port" => port,
|
56
|
+
"remote-debugging-address" => host,
|
57
|
+
# Doesn't work on MacOS, so we need to set it by CDP
|
58
|
+
"window-size" => options[:window_size].join(","),
|
59
|
+
"user-data-dir" => user_data_dir)
|
60
|
+
end
|
61
|
+
|
62
|
+
def merge_default(flags, options)
|
63
|
+
unless options.fetch(:headless, true)
|
64
|
+
defaults = except("headless", "disable-gpu")
|
65
|
+
end
|
66
|
+
|
67
|
+
defaults ||= DEFAULT_OPTIONS
|
68
|
+
defaults.merge(flags)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ferrum
|
4
|
+
class Browser
|
5
|
+
module Options
|
6
|
+
class Firefox < Base
|
7
|
+
DEFAULT_OPTIONS = {
|
8
|
+
"headless" => nil,
|
9
|
+
}.freeze
|
10
|
+
|
11
|
+
MAC_BIN_PATH = [
|
12
|
+
"/Applications/Firefox.app/Contents/MacOS/firefox-bin"
|
13
|
+
].freeze
|
14
|
+
LINUX_BIN_PATH = %w[firefox].freeze
|
15
|
+
|
16
|
+
def merge_required(flags, options, user_data_dir)
|
17
|
+
port = options.fetch(:port, BROWSER_PORT)
|
18
|
+
host = options.fetch(:host, BROWSER_HOST)
|
19
|
+
flags.merge("remote-debugger" => "#{host}:#{port}",
|
20
|
+
"profile" => user_data_dir)
|
21
|
+
end
|
22
|
+
|
23
|
+
def merge_default(flags, options)
|
24
|
+
unless options.fetch(:headless, true)
|
25
|
+
defaults = except("headless")
|
26
|
+
end
|
27
|
+
|
28
|
+
defaults ||= DEFAULT_OPTIONS
|
29
|
+
defaults.merge(flags)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -6,9 +6,10 @@ require "json"
|
|
6
6
|
require "addressable"
|
7
7
|
require "tmpdir"
|
8
8
|
require "forwardable"
|
9
|
+
require "ferrum/browser/options/base"
|
10
|
+
require "ferrum/browser/options/chrome"
|
11
|
+
require "ferrum/browser/options/firefox"
|
9
12
|
require "ferrum/browser/command"
|
10
|
-
require "ferrum/browser/chrome"
|
11
|
-
require "ferrum/browser/firefox"
|
12
13
|
|
13
14
|
module Ferrum
|
14
15
|
class Browser
|
@@ -19,7 +20,7 @@ module Ferrum
|
|
19
20
|
|
20
21
|
attr_reader :host, :port, :ws_url, :pid, :command,
|
21
22
|
:default_user_agent, :browser_version, :protocol_version,
|
22
|
-
:v8_version, :webkit_version
|
23
|
+
:v8_version, :webkit_version, :xvfb
|
23
24
|
|
24
25
|
|
25
26
|
extend Forwardable
|
@@ -81,7 +82,12 @@ module Ferrum
|
|
81
82
|
process_options[:pgroup] = true unless Ferrum.windows?
|
82
83
|
process_options[:out] = process_options[:err] = write_io
|
83
84
|
|
84
|
-
@
|
85
|
+
if @command.xvfb?
|
86
|
+
@xvfb = Xvfb.start(@command.options)
|
87
|
+
ObjectSpace.define_finalizer(self, self.class.process_killer(@xvfb.pid))
|
88
|
+
end
|
89
|
+
|
90
|
+
@pid = ::Process.spawn(Hash(@xvfb&.to_env), *@command.to_a, process_options)
|
85
91
|
ObjectSpace.define_finalizer(self, self.class.process_killer(@pid))
|
86
92
|
|
87
93
|
parse_ws_url(read_io, @process_timeout)
|
@@ -92,7 +98,12 @@ module Ferrum
|
|
92
98
|
end
|
93
99
|
|
94
100
|
def stop
|
95
|
-
|
101
|
+
if @pid
|
102
|
+
kill(@pid)
|
103
|
+
kill(@xvfb.pid) if @xvfb&.pid
|
104
|
+
@pid = nil
|
105
|
+
end
|
106
|
+
|
96
107
|
remove_user_data_dir if @user_data_dir
|
97
108
|
ObjectSpace.undefine_finalizer(self)
|
98
109
|
end
|
@@ -104,9 +115,8 @@ module Ferrum
|
|
104
115
|
|
105
116
|
private
|
106
117
|
|
107
|
-
def kill
|
108
|
-
self.class.process_killer(
|
109
|
-
@pid = nil
|
118
|
+
def kill(pid)
|
119
|
+
self.class.process_killer(pid).call
|
110
120
|
end
|
111
121
|
|
112
122
|
def remove_user_data_dir
|
@@ -8,6 +8,7 @@ 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
|
|
@@ -20,6 +21,10 @@ module Ferrum
|
|
20
21
|
@driver = ::WebSocket::Driver.client(self, max_length: max_receive_size)
|
21
22
|
@messages = Queue.new
|
22
23
|
|
24
|
+
if SKIP_LOGGING_SCREENSHOTS
|
25
|
+
@screenshot_commands = Concurrent::Hash.new
|
26
|
+
end
|
27
|
+
|
23
28
|
@driver.on(:open, &method(:on_open))
|
24
29
|
@driver.on(:message, &method(:on_message))
|
25
30
|
@driver.on(:close, &method(:on_close))
|
@@ -50,7 +55,14 @@ module Ferrum
|
|
50
55
|
def on_message(event)
|
51
56
|
data = JSON.parse(event.data)
|
52
57
|
@messages.push(data)
|
53
|
-
|
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")
|
54
66
|
end
|
55
67
|
|
56
68
|
def on_close(_event)
|
@@ -59,6 +71,10 @@ module Ferrum
|
|
59
71
|
end
|
60
72
|
|
61
73
|
def send_message(data)
|
74
|
+
if SKIP_LOGGING_SCREENSHOTS
|
75
|
+
@screenshot_commands[data[:id]] = true
|
76
|
+
end
|
77
|
+
|
62
78
|
json = data.to_json
|
63
79
|
@driver.text(json)
|
64
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/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
|
@@ -215,7 +217,7 @@ module Ferrum
|
|
215
217
|
|
216
218
|
def reduce_props(object_id, to)
|
217
219
|
if cyclic?(object_id).dig("result", "value")
|
218
|
-
return
|
220
|
+
return to.is_a?(Array) ? [cyclic_object] : cyclic_object
|
219
221
|
else
|
220
222
|
props = @page.command("Runtime.getProperties", ownProperties: true, objectId: object_id)
|
221
223
|
props["result"].reduce(to) do |memo, prop|
|
@@ -259,6 +261,18 @@ module Ferrum
|
|
259
261
|
JS
|
260
262
|
)
|
261
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>)
|
262
276
|
end
|
263
277
|
end
|
264
278
|
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)
|
@@ -121,16 +124,42 @@ module Ferrum
|
|
121
124
|
end
|
122
125
|
|
123
126
|
def find_position(x: nil, y: nil, position: :top)
|
124
|
-
|
125
|
-
|
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)
|
126
155
|
x = y = nil
|
127
156
|
|
128
157
|
if offset_x && offset_y && position == :top
|
129
|
-
point =
|
158
|
+
point = points.first
|
130
159
|
x = point[:x] + offset_x.to_i
|
131
160
|
y = point[:y] + offset_y.to_i
|
132
161
|
else
|
133
|
-
x, y =
|
162
|
+
x, y = points.inject([0, 0]) do |memo, point|
|
134
163
|
[memo[0] + point[:x],
|
135
164
|
memo[1] + point[:y]]
|
136
165
|
end
|
@@ -147,19 +176,11 @@ module Ferrum
|
|
147
176
|
[x, y]
|
148
177
|
end
|
149
178
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
# FIXME: Case when a few quads returned
|
157
|
-
result["quads"].map do |quad|
|
158
|
-
[{x: quad[0], y: quad[1]},
|
159
|
-
{x: quad[2], y: quad[3]},
|
160
|
-
{x: quad[4], y: quad[5]},
|
161
|
-
{x: quad[6], y: quad[7]}]
|
162
|
-
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]}]
|
163
184
|
end
|
164
185
|
end
|
165
186
|
end
|
data/lib/ferrum/page.rb
CHANGED
@@ -44,7 +44,7 @@ module Ferrum
|
|
44
44
|
|
45
45
|
def initialize(target_id, browser)
|
46
46
|
@frames = {}
|
47
|
-
@main_frame = Frame.new(nil, self)
|
47
|
+
@main_frame = Frame.new(nil, self)
|
48
48
|
@target_id, @browser = target_id, browser
|
49
49
|
@event = Event.new.tap(&:set)
|
50
50
|
|
@@ -125,6 +125,12 @@ module Ferrum
|
|
125
125
|
history_navigate(delta: 1)
|
126
126
|
end
|
127
127
|
|
128
|
+
def wait_for_reload(sec = 1)
|
129
|
+
@event.reset if @event.set?
|
130
|
+
@event.wait(sec)
|
131
|
+
@event.set
|
132
|
+
end
|
133
|
+
|
128
134
|
def bypass_csp(value = true)
|
129
135
|
enabled = !!value
|
130
136
|
command("Page.setBypassCSP", enabled: enabled)
|
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.9'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dmitry Vorotilin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-07-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: websocket-driver
|
@@ -64,14 +64,14 @@ dependencies:
|
|
64
64
|
requirements:
|
65
65
|
- - "~>"
|
66
66
|
- !ruby/object:Gem::Version
|
67
|
-
version: '2.
|
67
|
+
version: '2.5'
|
68
68
|
type: :runtime
|
69
69
|
prerelease: false
|
70
70
|
version_requirements: !ruby/object:Gem::Requirement
|
71
71
|
requirements:
|
72
72
|
- - "~>"
|
73
73
|
- !ruby/object:Gem::Version
|
74
|
-
version: '2.
|
74
|
+
version: '2.5'
|
75
75
|
- !ruby/object:Gem::Dependency
|
76
76
|
name: rake
|
77
77
|
requirement: !ruby/object:Gem::Requirement
|
@@ -181,13 +181,15 @@ files:
|
|
181
181
|
- README.md
|
182
182
|
- lib/ferrum.rb
|
183
183
|
- lib/ferrum/browser.rb
|
184
|
-
- lib/ferrum/browser/chrome.rb
|
185
184
|
- lib/ferrum/browser/client.rb
|
186
185
|
- lib/ferrum/browser/command.rb
|
187
|
-
- lib/ferrum/browser/
|
186
|
+
- lib/ferrum/browser/options/base.rb
|
187
|
+
- lib/ferrum/browser/options/chrome.rb
|
188
|
+
- lib/ferrum/browser/options/firefox.rb
|
188
189
|
- lib/ferrum/browser/process.rb
|
189
190
|
- lib/ferrum/browser/subscriber.rb
|
190
191
|
- lib/ferrum/browser/web_socket.rb
|
192
|
+
- lib/ferrum/browser/xvfb.rb
|
191
193
|
- lib/ferrum/context.rb
|
192
194
|
- lib/ferrum/contexts.rb
|
193
195
|
- lib/ferrum/cookies.rb
|
@@ -1,76 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Ferrum
|
4
|
-
class Browser
|
5
|
-
class Chrome < Command
|
6
|
-
DEFAULT_OPTIONS = {
|
7
|
-
"headless" => nil,
|
8
|
-
"disable-gpu" => nil,
|
9
|
-
"hide-scrollbars" => nil,
|
10
|
-
"mute-audio" => nil,
|
11
|
-
"enable-automation" => nil,
|
12
|
-
"disable-web-security" => nil,
|
13
|
-
"disable-session-crashed-bubble" => nil,
|
14
|
-
"disable-breakpad" => nil,
|
15
|
-
"disable-sync" => nil,
|
16
|
-
"no-first-run" => nil,
|
17
|
-
"use-mock-keychain" => nil,
|
18
|
-
"keep-alive-for-test" => nil,
|
19
|
-
"disable-popup-blocking" => nil,
|
20
|
-
"disable-extensions" => nil,
|
21
|
-
"disable-hang-monitor" => nil,
|
22
|
-
"disable-features" => "site-per-process,TranslateUI",
|
23
|
-
"disable-translate" => nil,
|
24
|
-
"disable-background-networking" => nil,
|
25
|
-
"enable-features" => "NetworkService,NetworkServiceInProcess",
|
26
|
-
"disable-background-timer-throttling" => nil,
|
27
|
-
"disable-backgrounding-occluded-windows" => nil,
|
28
|
-
"disable-client-side-phishing-detection" => nil,
|
29
|
-
"disable-default-apps" => nil,
|
30
|
-
"disable-dev-shm-usage" => nil,
|
31
|
-
"disable-ipc-flooding-protection" => nil,
|
32
|
-
"disable-prompt-on-repost" => nil,
|
33
|
-
"disable-renderer-backgrounding" => nil,
|
34
|
-
"force-color-profile" => "srgb",
|
35
|
-
"metrics-recording-only" => nil,
|
36
|
-
"safebrowsing-disable-auto-update" => nil,
|
37
|
-
"password-store" => "basic",
|
38
|
-
# Note: --no-sandbox is not needed if you properly setup a user in the container.
|
39
|
-
# https://github.com/ebidel/lighthouse-ci/blob/master/builder/Dockerfile#L35-L40
|
40
|
-
# "no-sandbox" => nil,
|
41
|
-
}.freeze
|
42
|
-
|
43
|
-
MAC_BIN_PATH = [
|
44
|
-
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
45
|
-
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
46
|
-
].freeze
|
47
|
-
LINUX_BIN_PATH = %w[chromium google-chrome-unstable google-chrome-beta
|
48
|
-
google-chrome chrome chromium-browser
|
49
|
-
google-chrome-stable].freeze
|
50
|
-
|
51
|
-
private
|
52
|
-
|
53
|
-
def combine_flags
|
54
|
-
# Doesn't work on MacOS, so we need to set it by CDP as well
|
55
|
-
@flags.merge!("window-size" => options[:window_size].join(","))
|
56
|
-
|
57
|
-
port = options.fetch(:port, BROWSER_PORT)
|
58
|
-
@flags.merge!("remote-debugging-port" => port)
|
59
|
-
|
60
|
-
host = options.fetch(:host, BROWSER_HOST)
|
61
|
-
@flags.merge!("remote-debugging-address" => host)
|
62
|
-
|
63
|
-
@flags.merge!("user-data-dir" => @user_data_dir)
|
64
|
-
|
65
|
-
@flags = DEFAULT_OPTIONS.merge(@flags)
|
66
|
-
|
67
|
-
unless options.fetch(:headless, true)
|
68
|
-
@flags.delete("headless")
|
69
|
-
@flags.delete("disable-gpu")
|
70
|
-
end
|
71
|
-
|
72
|
-
@flags.merge!(options.fetch(:browser_options, {}))
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
@@ -1,34 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Ferrum
|
4
|
-
class Browser
|
5
|
-
class Firefox < Command
|
6
|
-
DEFAULT_OPTIONS = {
|
7
|
-
"headless" => nil,
|
8
|
-
}.freeze
|
9
|
-
|
10
|
-
MAC_BIN_PATH = [
|
11
|
-
"/Applications/Firefox.app/Contents/MacOS/firefox-bin"
|
12
|
-
].freeze
|
13
|
-
LINUX_BIN_PATH = %w[firefox].freeze
|
14
|
-
|
15
|
-
private
|
16
|
-
|
17
|
-
def combine_flags
|
18
|
-
port = options.fetch(:port, BROWSER_PORT)
|
19
|
-
host = options.fetch(:host, BROWSER_HOST)
|
20
|
-
@flags.merge!("remote-debugger" => "#{host}:#{port}")
|
21
|
-
|
22
|
-
@flags.merge!("profile" => @user_data_dir)
|
23
|
-
|
24
|
-
@flags = DEFAULT_OPTIONS.merge(@flags)
|
25
|
-
|
26
|
-
unless options.fetch(:headless, true)
|
27
|
-
@flags.delete("headless")
|
28
|
-
end
|
29
|
-
|
30
|
-
@flags.merge!(options.fetch(:browser_options, {}))
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|