ferrum 0.11 → 0.12
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/LICENSE +1 -1
- data/README.md +178 -29
- data/lib/ferrum/browser/binary.rb +46 -0
- data/lib/ferrum/browser/client.rb +13 -12
- data/lib/ferrum/browser/command.rb +7 -8
- data/lib/ferrum/browser/options/base.rb +1 -7
- data/lib/ferrum/browser/options/chrome.rb +17 -11
- data/lib/ferrum/browser/options/firefox.rb +11 -4
- data/lib/ferrum/browser/process.rb +41 -35
- data/lib/ferrum/browser/subscriber.rb +1 -3
- data/lib/ferrum/browser/web_socket.rb +9 -12
- data/lib/ferrum/browser/xvfb.rb +4 -8
- data/lib/ferrum/browser.rb +44 -12
- data/lib/ferrum/context.rb +6 -2
- data/lib/ferrum/contexts.rb +10 -8
- data/lib/ferrum/cookies.rb +10 -9
- data/lib/ferrum/errors.rb +115 -0
- data/lib/ferrum/frame/runtime.rb +20 -17
- data/lib/ferrum/frame.rb +32 -24
- data/lib/ferrum/headers.rb +2 -2
- data/lib/ferrum/keyboard.rb +11 -11
- data/lib/ferrum/mouse.rb +8 -7
- data/lib/ferrum/network/auth_request.rb +7 -2
- data/lib/ferrum/network/exchange.rb +14 -10
- data/lib/ferrum/network/intercepted_request.rb +10 -8
- data/lib/ferrum/network/request.rb +5 -0
- data/lib/ferrum/network/response.rb +4 -4
- data/lib/ferrum/network.rb +124 -35
- data/lib/ferrum/node.rb +69 -23
- data/lib/ferrum/page/animation.rb +0 -1
- data/lib/ferrum/page/frames.rb +46 -20
- data/lib/ferrum/page/screenshot.rb +51 -65
- data/lib/ferrum/page/stream.rb +38 -0
- data/lib/ferrum/page/tracing.rb +71 -0
- data/lib/ferrum/page.rb +81 -36
- data/lib/ferrum/proxy.rb +58 -0
- data/lib/ferrum/{rbga.rb → rgba.rb} +4 -2
- data/lib/ferrum/target.rb +1 -0
- data/lib/ferrum/utils/attempt.rb +20 -0
- data/lib/ferrum/utils/elapsed_time.rb +27 -0
- data/lib/ferrum/utils/platform.rb +28 -0
- data/lib/ferrum/version.rb +1 -1
- data/lib/ferrum.rb +4 -146
- metadata +60 -51
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "ferrum/
|
3
|
+
require "ferrum/rgba"
|
4
4
|
|
5
5
|
module Ferrum
|
6
6
|
class Page
|
@@ -13,21 +13,19 @@ module Ferrum
|
|
13
13
|
}.freeze
|
14
14
|
|
15
15
|
PAPER_FORMATS = {
|
16
|
-
letter:
|
17
|
-
legal:
|
18
|
-
tabloid:
|
19
|
-
ledger:
|
20
|
-
A0:
|
21
|
-
A1:
|
22
|
-
A2:
|
23
|
-
A3:
|
24
|
-
A4:
|
25
|
-
A5:
|
26
|
-
A6:
|
16
|
+
letter: { width: 8.50, height: 11.00 },
|
17
|
+
legal: { width: 8.50, height: 14.00 },
|
18
|
+
tabloid: { width: 11.00, height: 17.00 },
|
19
|
+
ledger: { width: 17.00, height: 11.00 },
|
20
|
+
A0: { width: 33.10, height: 46.80 },
|
21
|
+
A1: { width: 23.40, height: 33.10 },
|
22
|
+
A2: { width: 16.54, height: 23.40 },
|
23
|
+
A3: { width: 11.70, height: 16.54 },
|
24
|
+
A4: { width: 8.27, height: 11.70 },
|
25
|
+
A5: { width: 5.83, height: 8.27 },
|
26
|
+
A6: { width: 4.13, height: 5.83 }
|
27
27
|
}.freeze
|
28
28
|
|
29
|
-
STREAM_CHUNK = 128 * 1024
|
30
|
-
|
31
29
|
def screenshot(**opts)
|
32
30
|
path, encoding = common_options(**opts)
|
33
31
|
options = screenshot_options(path, **opts)
|
@@ -42,17 +40,13 @@ module Ferrum
|
|
42
40
|
path, encoding = common_options(**opts)
|
43
41
|
options = pdf_options(**opts).merge(transferMode: "ReturnAsStream")
|
44
42
|
handle = command("Page.printToPDF", **options).fetch("stream")
|
45
|
-
|
46
|
-
if path
|
47
|
-
stream_to_file(handle, path: path)
|
48
|
-
else
|
49
|
-
stream_to_memory(handle, encoding: encoding)
|
50
|
-
end
|
43
|
+
stream_to(path: path, encoding: encoding, handle: handle)
|
51
44
|
end
|
52
45
|
|
53
46
|
def mhtml(path: nil)
|
54
47
|
data = command("Page.captureSnapshot", format: :mhtml).fetch("data")
|
55
48
|
return data if path.nil?
|
49
|
+
|
56
50
|
save_file(path, data)
|
57
51
|
end
|
58
52
|
|
@@ -73,30 +67,8 @@ module Ferrum
|
|
73
67
|
|
74
68
|
def save_file(path, data)
|
75
69
|
return data unless path
|
76
|
-
File.open(path.to_s, "wb") { |f| f.write(data) }
|
77
|
-
end
|
78
70
|
|
79
|
-
|
80
|
-
File.open(path, "wb") { |f| stream_to(handle, f) }
|
81
|
-
true
|
82
|
-
end
|
83
|
-
|
84
|
-
def stream_to_memory(handle, encoding:)
|
85
|
-
data = String.new("") # Mutable string has << and compatible to File
|
86
|
-
stream_to(handle, data)
|
87
|
-
encoding == :base64 ? Base64.encode64(data) : data
|
88
|
-
end
|
89
|
-
|
90
|
-
def stream_to(handle, output)
|
91
|
-
loop do
|
92
|
-
result = command("IO.read", handle: handle, size: STREAM_CHUNK)
|
93
|
-
|
94
|
-
data_chunk = result["data"]
|
95
|
-
data_chunk = Base64.decode64(data_chunk) if result["base64Encoded"]
|
96
|
-
output << data_chunk
|
97
|
-
|
98
|
-
break if result["eof"]
|
99
|
-
end
|
71
|
+
File.binwrite(path.to_s, data)
|
100
72
|
end
|
101
73
|
|
102
74
|
def common_options(encoding: :base64, path: nil, **_)
|
@@ -119,44 +91,57 @@ module Ferrum
|
|
119
91
|
paper_height: dimension[:height])
|
120
92
|
end
|
121
93
|
|
122
|
-
options.
|
94
|
+
options.transform_keys { |k| to_camel_case(k) }
|
123
95
|
end
|
124
96
|
|
125
|
-
def screenshot_options(path = nil, format: nil, scale: 1.0, **
|
126
|
-
|
97
|
+
def screenshot_options(path = nil, format: nil, scale: 1.0, **options)
|
98
|
+
screenshot_options = {}
|
99
|
+
|
100
|
+
format, quality = format_options(format, path, options[:quality])
|
101
|
+
screenshot_options.merge!(quality: quality) if quality
|
102
|
+
screenshot_options.merge!(format: format)
|
103
|
+
|
104
|
+
clip = area_options(options[:full], options[:selector], scale)
|
105
|
+
screenshot_options.merge!(clip: clip) if clip
|
106
|
+
|
107
|
+
screenshot_options
|
108
|
+
end
|
127
109
|
|
110
|
+
def format_options(format, path, quality)
|
128
111
|
format ||= path ? File.extname(path).delete(".") : "png"
|
129
112
|
format = "jpeg" if format == "jpg"
|
130
113
|
raise "Not supported options `:format` #{format}. jpeg | png" if format !~ /jpeg|png/i
|
131
|
-
options.merge!(format: format)
|
132
114
|
|
133
|
-
|
115
|
+
quality ||= 75 if format == "jpeg"
|
134
116
|
|
135
|
-
|
136
|
-
|
137
|
-
end
|
117
|
+
[format, quality]
|
118
|
+
end
|
138
119
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
elsif opts[:selector]
|
143
|
-
options.merge!(clip: get_bounding_rect(opts[:selector]).merge(scale: scale))
|
144
|
-
end
|
120
|
+
def area_options(full, selector, scale)
|
121
|
+
message = "Ignoring :selector in #screenshot since full: true was given at #{caller(1..1).first}"
|
122
|
+
warn(message) if full && selector
|
145
123
|
|
146
|
-
|
147
|
-
|
124
|
+
clip = if full
|
125
|
+
width, height = document_size
|
126
|
+
{ x: 0, y: 0, width: width, height: height, scale: scale } if width.positive? && height.positive?
|
127
|
+
elsif selector
|
128
|
+
bounding_rect(selector).merge(scale: scale)
|
129
|
+
end
|
130
|
+
|
131
|
+
if scale != 1
|
132
|
+
unless clip
|
148
133
|
width, height = viewport_size
|
149
|
-
|
134
|
+
clip = { x: 0, y: 0, width: width, height: height }
|
150
135
|
end
|
151
136
|
|
152
|
-
|
137
|
+
clip.merge!(scale: scale)
|
153
138
|
end
|
154
139
|
|
155
|
-
|
140
|
+
clip
|
156
141
|
end
|
157
142
|
|
158
|
-
def
|
159
|
-
rect = evaluate_async(%
|
143
|
+
def bounding_rect(selector)
|
144
|
+
rect = evaluate_async(%(
|
160
145
|
const rect = document
|
161
146
|
.querySelector('#{selector}')
|
162
147
|
.getBoundingClientRect();
|
@@ -169,7 +154,8 @@ module Ferrum
|
|
169
154
|
|
170
155
|
def to_camel_case(option)
|
171
156
|
return :preferCSSPageSize if option == :prefer_css_page_size
|
172
|
-
|
157
|
+
|
158
|
+
option.to_s.gsub(%r{(?:_|(/))([a-z\d]*)}) { "#{Regexp.last_match(1)}#{Regexp.last_match(2).capitalize}" }.to_sym
|
173
159
|
end
|
174
160
|
|
175
161
|
def capture_screenshot(options, full, background_color)
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ferrum
|
4
|
+
class Page
|
5
|
+
module Stream
|
6
|
+
STREAM_CHUNK = 128 * 1024
|
7
|
+
|
8
|
+
def stream_to(path:, encoding:, handle:)
|
9
|
+
if path.nil?
|
10
|
+
stream_to_memory(encoding: encoding, handle: handle)
|
11
|
+
else
|
12
|
+
stream_to_file(path: path, handle: handle)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def stream_to_file(path:, handle:)
|
17
|
+
File.open(path, "wb") { |f| stream(output: f, handle: handle) }
|
18
|
+
true
|
19
|
+
end
|
20
|
+
|
21
|
+
def stream_to_memory(encoding:, handle:)
|
22
|
+
data = String.new # Mutable string has << and compatible to File
|
23
|
+
stream(output: data, handle: handle)
|
24
|
+
encoding == :base64 ? Base64.encode64(data) : data
|
25
|
+
end
|
26
|
+
|
27
|
+
def stream(output:, handle:)
|
28
|
+
loop do
|
29
|
+
result = command("IO.read", handle: handle, size: STREAM_CHUNK)
|
30
|
+
chunk = result.fetch("data")
|
31
|
+
chunk = Base64.decode64(chunk) if result["base64Encoded"]
|
32
|
+
output << chunk
|
33
|
+
break if result["eof"]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ferrum
|
4
|
+
class Page
|
5
|
+
class Tracing
|
6
|
+
EXCLUDED_CATEGORIES = %w[*].freeze
|
7
|
+
SCREENSHOT_CATEGORIES = %w[disabled-by-default-devtools.screenshot].freeze
|
8
|
+
INCLUDED_CATEGORIES = %w[devtools.timeline v8.execute disabled-by-default-devtools.timeline
|
9
|
+
disabled-by-default-devtools.timeline.frame toplevel blink.console
|
10
|
+
blink.user_timing latencyInfo disabled-by-default-devtools.timeline.stack
|
11
|
+
disabled-by-default-v8.cpu_profiler disabled-by-default-v8.cpu_profiler.hires].freeze
|
12
|
+
DEFAULT_TRACE_CONFIG = {
|
13
|
+
includedCategories: INCLUDED_CATEGORIES,
|
14
|
+
excludedCategories: EXCLUDED_CATEGORIES
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
def initialize(page)
|
18
|
+
@page = page
|
19
|
+
@subscribed_tracing_complete = false
|
20
|
+
end
|
21
|
+
|
22
|
+
def record(path: nil, encoding: :binary, timeout: nil, trace_config: nil, screenshots: false)
|
23
|
+
@path = path
|
24
|
+
@encoding = encoding
|
25
|
+
@pending = Concurrent::IVar.new
|
26
|
+
trace_config ||= DEFAULT_TRACE_CONFIG.dup
|
27
|
+
|
28
|
+
if screenshots
|
29
|
+
included = trace_config.fetch(:includedCategories, [])
|
30
|
+
trace_config.merge!(includedCategories: included | SCREENSHOT_CATEGORIES)
|
31
|
+
end
|
32
|
+
|
33
|
+
subscribe_tracing_complete
|
34
|
+
|
35
|
+
start(trace_config)
|
36
|
+
yield
|
37
|
+
stop
|
38
|
+
|
39
|
+
@pending.value!(timeout || @page.timeout)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def start(config)
|
45
|
+
@page.command("Tracing.start", transferMode: "ReturnAsStream", traceConfig: config)
|
46
|
+
end
|
47
|
+
|
48
|
+
def stop
|
49
|
+
@page.command("Tracing.end")
|
50
|
+
end
|
51
|
+
|
52
|
+
def subscribe_tracing_complete
|
53
|
+
return if @subscribed_tracing_complete
|
54
|
+
|
55
|
+
@page.on("Tracing.tracingComplete") do |event, index|
|
56
|
+
next if index.to_i != 0
|
57
|
+
|
58
|
+
@pending.set(stream_handle(event["stream"]))
|
59
|
+
rescue StandardError => e
|
60
|
+
@pending.fail(e)
|
61
|
+
end
|
62
|
+
|
63
|
+
@subscribed_tracing_complete = true
|
64
|
+
end
|
65
|
+
|
66
|
+
def stream_handle(handle)
|
67
|
+
@page.stream_to(path: @path, encoding: @encoding, handle: handle)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/lib/ferrum/page.rb
CHANGED
@@ -10,6 +10,8 @@ require "ferrum/network"
|
|
10
10
|
require "ferrum/page/frames"
|
11
11
|
require "ferrum/page/screenshot"
|
12
12
|
require "ferrum/page/animation"
|
13
|
+
require "ferrum/page/tracing"
|
14
|
+
require "ferrum/page/stream"
|
13
15
|
require "ferrum/browser/client"
|
14
16
|
|
15
17
|
module Ferrum
|
@@ -32,21 +34,26 @@ module Ferrum
|
|
32
34
|
|
33
35
|
extend Forwardable
|
34
36
|
delegate %i[at_css at_xpath css xpath
|
35
|
-
current_url current_title url title body doctype
|
37
|
+
current_url current_title url title body doctype content=
|
36
38
|
execution_id evaluate evaluate_on evaluate_async execute evaluate_func
|
37
39
|
add_script_tag add_style_tag] => :main_frame
|
38
40
|
|
39
|
-
include
|
41
|
+
include Animation
|
42
|
+
include Screenshot
|
43
|
+
include Frames
|
44
|
+
include Stream
|
40
45
|
|
41
46
|
attr_accessor :referrer
|
42
47
|
attr_reader :target_id, :browser,
|
43
48
|
:headers, :cookies, :network,
|
44
|
-
:mouse, :keyboard, :event,
|
49
|
+
:mouse, :keyboard, :event,
|
50
|
+
:tracing
|
45
51
|
|
46
52
|
def initialize(target_id, browser)
|
47
53
|
@frames = {}
|
48
54
|
@main_frame = Frame.new(nil, self)
|
49
|
-
@
|
55
|
+
@browser = browser
|
56
|
+
@target_id = target_id
|
50
57
|
@event = Event.new.tap(&:set)
|
51
58
|
|
52
59
|
host = @browser.process.host
|
@@ -54,9 +61,12 @@ module Ferrum
|
|
54
61
|
ws_url = "ws://#{host}:#{port}/devtools/page/#{@target_id}"
|
55
62
|
@client = Browser::Client.new(browser, ws_url, id_starts_with: 1000)
|
56
63
|
|
57
|
-
@mouse
|
58
|
-
@
|
64
|
+
@mouse = Mouse.new(self)
|
65
|
+
@keyboard = Keyboard.new(self)
|
66
|
+
@headers = Headers.new(self)
|
67
|
+
@cookies = Cookies.new(self)
|
59
68
|
@network = Network.new(self)
|
69
|
+
@tracing = Tracing.new(self)
|
60
70
|
|
61
71
|
subscribe
|
62
72
|
prepare_page
|
@@ -66,6 +76,10 @@ module Ferrum
|
|
66
76
|
@browser.timeout
|
67
77
|
end
|
68
78
|
|
79
|
+
def context
|
80
|
+
@browser.contexts.find_by(target_id: target_id)
|
81
|
+
end
|
82
|
+
|
69
83
|
def go_to(url = nil)
|
70
84
|
options = { url: combine_url!(url) }
|
71
85
|
options.merge!(referrer: referrer) if referrer
|
@@ -77,6 +91,7 @@ module Ferrum
|
|
77
91
|
net::ERR_CONNECTION_TIMED_OUT].include?(response["errorText"])
|
78
92
|
raise StatusError, options[:url]
|
79
93
|
end
|
94
|
+
|
80
95
|
response["frameId"]
|
81
96
|
rescue TimeoutError
|
82
97
|
if @browser.pending_connection_errors
|
@@ -85,6 +100,7 @@ module Ferrum
|
|
85
100
|
end
|
86
101
|
end
|
87
102
|
alias goto go_to
|
103
|
+
alias go go_to
|
88
104
|
|
89
105
|
def close
|
90
106
|
@headers.clear
|
@@ -113,14 +129,16 @@ module Ferrum
|
|
113
129
|
@browser.command("Browser.getWindowBounds", windowId: window_id).fetch("bounds").values_at("left", "top")
|
114
130
|
end
|
115
131
|
|
116
|
-
def position=(
|
117
|
-
@browser.command("Browser.setWindowBounds",
|
132
|
+
def position=(options)
|
133
|
+
@browser.command("Browser.setWindowBounds",
|
134
|
+
windowId: window_id,
|
135
|
+
bounds: { left: options[:left], top: options[:top] })
|
118
136
|
end
|
119
137
|
|
120
138
|
def refresh
|
121
139
|
command("Page.reload", wait: timeout, slowmoable: true)
|
122
140
|
end
|
123
|
-
|
141
|
+
alias reload refresh
|
124
142
|
|
125
143
|
def stop
|
126
144
|
command("Page.stopLoading", slowmoable: true)
|
@@ -140,8 +158,7 @@ module Ferrum
|
|
140
158
|
@event.set
|
141
159
|
end
|
142
160
|
|
143
|
-
def bypass_csp(
|
144
|
-
enabled = !!value
|
161
|
+
def bypass_csp(enabled: true)
|
145
162
|
command("Page.setBypassCSP", enabled: enabled)
|
146
163
|
enabled
|
147
164
|
end
|
@@ -155,14 +172,15 @@ module Ferrum
|
|
155
172
|
end
|
156
173
|
|
157
174
|
def command(method, wait: 0, slowmoable: false, **params)
|
158
|
-
iteration = @event.reset if wait
|
159
|
-
sleep(@browser.slowmo) if slowmoable && @browser.slowmo
|
175
|
+
iteration = @event.reset if wait.positive?
|
176
|
+
sleep(@browser.slowmo) if slowmoable && @browser.slowmo.positive?
|
160
177
|
result = @client.command(method, params)
|
161
178
|
|
162
|
-
if wait
|
163
|
-
@event.wait(wait)
|
164
|
-
|
165
|
-
|
179
|
+
if wait.positive?
|
180
|
+
@event.wait(wait)
|
181
|
+
# Wait a bit after command and check if iteration has
|
182
|
+
# changed which means there was some network event for
|
183
|
+
# the main frame and it started to load new content.
|
166
184
|
if iteration != @event.iteration
|
167
185
|
set = @event.wait(@browser.timeout)
|
168
186
|
raise TimeoutError unless set
|
@@ -214,8 +232,19 @@ module Ferrum
|
|
214
232
|
|
215
233
|
if @browser.js_errors
|
216
234
|
on("Runtime.exceptionThrown") do |params|
|
217
|
-
# FIXME https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/
|
218
|
-
Thread.main.raise JavaScriptError.new(
|
235
|
+
# FIXME: https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/
|
236
|
+
Thread.main.raise JavaScriptError.new(
|
237
|
+
params.dig("exceptionDetails", "exception"),
|
238
|
+
params.dig("exceptionDetails", "stackTrace")
|
239
|
+
)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
on(:dialog) do |dialog, _index, total|
|
244
|
+
if total == 1
|
245
|
+
warn "Dialog was shown but you didn't provide `on(:dialog)` callback, accepting it by default. " \
|
246
|
+
"Please take a look at https://github.com/rubycdp/ferrum#dialog"
|
247
|
+
dialog.accept
|
219
248
|
end
|
220
249
|
end
|
221
250
|
end
|
@@ -228,8 +257,22 @@ module Ferrum
|
|
228
257
|
command("Log.enable")
|
229
258
|
command("Network.enable")
|
230
259
|
|
260
|
+
if @browser.proxy_options && @browser.proxy_options[:user] && @browser.proxy_options[:password]
|
261
|
+
auth_options = @browser.proxy_options.slice(:user, :password)
|
262
|
+
network.authorize(type: :proxy, **auth_options) do |request, _index, _total|
|
263
|
+
request.continue
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
231
267
|
if @browser.options[:save_path]
|
232
|
-
|
268
|
+
unless Pathname.new(@browser.options[:save_path]).absolute?
|
269
|
+
raise Error, "supply absolute path for `:save_path` option"
|
270
|
+
end
|
271
|
+
|
272
|
+
@browser.command("Browser.setDownloadBehavior",
|
273
|
+
browserContextId: context.id,
|
274
|
+
downloadPath: browser.options[:save_path],
|
275
|
+
behavior: "allow", eventsEnabled: true)
|
233
276
|
end
|
234
277
|
|
235
278
|
@browser.extensions.each do |extension|
|
@@ -242,14 +285,14 @@ module Ferrum
|
|
242
285
|
resize(width: width, height: height)
|
243
286
|
|
244
287
|
response = command("Page.getNavigationHistory")
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
288
|
+
return unless response.dig("entries", 0, "transitionType") != "typed"
|
289
|
+
|
290
|
+
# If we create page by clicking links, submitting forms and so on it
|
291
|
+
# opens a new window for which `frameStoppedLoading` event never
|
292
|
+
# occurs and thus search for nodes cannot be completed. Here we check
|
293
|
+
# the history and if the transitionType for example `link` then
|
294
|
+
# content is already loaded and we can try to get the document.
|
295
|
+
document_node_id
|
253
296
|
end
|
254
297
|
|
255
298
|
def inject_extensions
|
@@ -268,13 +311,15 @@ module Ferrum
|
|
268
311
|
def history_navigate(delta:)
|
269
312
|
history = command("Page.getNavigationHistory")
|
270
313
|
index, entries = history.values_at("currentIndex", "entries")
|
314
|
+
entry = entries[index + delta]
|
271
315
|
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
316
|
+
return unless entry
|
317
|
+
|
318
|
+
# Potential wait because of network event
|
319
|
+
command("Page.navigateToHistoryEntry",
|
320
|
+
wait: Mouse::CLICK_WAIT,
|
321
|
+
slowmoable: true,
|
322
|
+
entryId: entry["id"])
|
278
323
|
end
|
279
324
|
|
280
325
|
def combine_url!(url_or_path)
|
@@ -288,8 +333,8 @@ module Ferrum
|
|
288
333
|
(nil_or_relative ? @browser.base_url.join(url.to_s) : url).to_s
|
289
334
|
end
|
290
335
|
|
291
|
-
def
|
292
|
-
|
336
|
+
def document_node_id
|
337
|
+
command("DOM.getDocument", depth: 0).dig("root", "nodeId")
|
293
338
|
end
|
294
339
|
end
|
295
340
|
end
|
data/lib/ferrum/proxy.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tempfile"
|
4
|
+
require "webrick"
|
5
|
+
require "webrick/httpproxy"
|
6
|
+
|
7
|
+
module Ferrum
|
8
|
+
class Proxy
|
9
|
+
def self.start(**args)
|
10
|
+
new(**args).tap(&:start)
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :host, :port, :user, :password
|
14
|
+
|
15
|
+
def initialize(host: "127.0.0.1", port: 0, user: nil, password: nil)
|
16
|
+
@file = nil
|
17
|
+
@host = host
|
18
|
+
@port = port
|
19
|
+
@user = user
|
20
|
+
@password = password
|
21
|
+
at_exit { stop }
|
22
|
+
end
|
23
|
+
|
24
|
+
def start
|
25
|
+
options = {
|
26
|
+
ProxyURI: nil, ServerType: Thread,
|
27
|
+
Logger: Logger.new(IO::NULL), AccessLog: [],
|
28
|
+
BindAddress: host, Port: port
|
29
|
+
}
|
30
|
+
|
31
|
+
if user && password
|
32
|
+
@file = Tempfile.new("htpasswd")
|
33
|
+
htpasswd = WEBrick::HTTPAuth::Htpasswd.new(@file.path)
|
34
|
+
htpasswd.set_passwd "Proxy Realm", user, password
|
35
|
+
htpasswd.flush
|
36
|
+
authenticator = WEBrick::HTTPAuth::ProxyBasicAuth.new(Realm: "Proxy Realm",
|
37
|
+
UserDB: htpasswd,
|
38
|
+
Logger: Logger.new(IO::NULL))
|
39
|
+
options.merge!(ProxyAuthProc: authenticator.method(:authenticate).to_proc)
|
40
|
+
end
|
41
|
+
|
42
|
+
@server = WEBrick::HTTPProxyServer.new(**options)
|
43
|
+
@server.start
|
44
|
+
@port = @server.config[:Port]
|
45
|
+
end
|
46
|
+
|
47
|
+
def rotate(host:, port:, user: nil, password: nil)
|
48
|
+
credentials = "#{user}:#{password}@" if user && password
|
49
|
+
proxy_uri = "schema://#{credentials}#{host}:#{port}"
|
50
|
+
@server.config[:ProxyURI] = URI.parse(proxy_uri)
|
51
|
+
end
|
52
|
+
|
53
|
+
def stop
|
54
|
+
@file&.unlink
|
55
|
+
@server.shutdown
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Ferrum
|
2
4
|
class RGBA
|
3
5
|
def initialize(red, green, blue, alpha)
|
@@ -23,13 +25,13 @@ module Ferrum
|
|
23
25
|
end
|
24
26
|
|
25
27
|
def validate_color(value)
|
26
|
-
return if value
|
28
|
+
return if value.is_a?(Integer) && Range.new(0, 255).include?(value)
|
27
29
|
|
28
30
|
raise ArgumentError, "Wrong value of #{value} should be Integer from 0 to 255"
|
29
31
|
end
|
30
32
|
|
31
33
|
def validate_alpha
|
32
|
-
return if alpha
|
34
|
+
return if alpha.is_a?(Float) && Range.new(0.0, 1.0).include?(alpha)
|
33
35
|
|
34
36
|
raise ArgumentError,
|
35
37
|
"Wrong alpha value #{alpha} should be Float between 0.0 (fully transparent) and 1.0 (fully opaque)"
|
data/lib/ferrum/target.rb
CHANGED
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ferrum
|
4
|
+
module Utils
|
5
|
+
module Attempt
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def with_retry(errors:, max:, wait:)
|
9
|
+
attempts ||= 1
|
10
|
+
yield
|
11
|
+
rescue *Array(errors)
|
12
|
+
raise if attempts >= max
|
13
|
+
|
14
|
+
attempts += 1
|
15
|
+
sleep(wait)
|
16
|
+
retry
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "concurrent-ruby"
|
4
|
+
|
5
|
+
module Ferrum
|
6
|
+
module Utils
|
7
|
+
module ElapsedTime
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def start
|
11
|
+
@start ||= monotonic_time
|
12
|
+
end
|
13
|
+
|
14
|
+
def elapsed_time(start = nil)
|
15
|
+
monotonic_time - (start || @start)
|
16
|
+
end
|
17
|
+
|
18
|
+
def monotonic_time
|
19
|
+
Concurrent.monotonic_time
|
20
|
+
end
|
21
|
+
|
22
|
+
def timeout?(start, timeout)
|
23
|
+
elapsed_time(start) > timeout
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|