ferrum 0.8 → 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 +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
|