ferrum 0.10.1 → 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 +261 -28
- data/lib/ferrum/browser/binary.rb +46 -0
- data/lib/ferrum/browser/client.rb +15 -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 -10
- 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 +49 -12
- data/lib/ferrum/context.rb +12 -4
- data/lib/ferrum/contexts.rb +13 -9
- 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 +98 -40
- data/lib/ferrum/page/animation.rb +15 -0
- data/lib/ferrum/page/frames.rb +46 -15
- data/lib/ferrum/page/screenshot.rb +53 -67
- data/lib/ferrum/page/stream.rb +38 -0
- data/lib/ferrum/page/tracing.rb +71 -0
- data/lib/ferrum/page.rb +88 -34
- 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 -140
- metadata +65 -50
@@ -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
|
@@ -12,22 +12,20 @@ module Ferrum
|
|
12
12
|
scale: 1.0
|
13
13
|
}.freeze
|
14
14
|
|
15
|
-
|
16
|
-
letter:
|
17
|
-
legal:
|
18
|
-
tabloid:
|
19
|
-
ledger:
|
20
|
-
A0:
|
21
|
-
A1:
|
22
|
-
A2:
|
23
|
-
A3:
|
24
|
-
A4:
|
25
|
-
A5:
|
26
|
-
A6:
|
15
|
+
PAPER_FORMATS = {
|
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, **_)
|
@@ -114,49 +86,62 @@ module Ferrum
|
|
114
86
|
raise ArgumentError, "Specify :format or :paper_width, :paper_height"
|
115
87
|
end
|
116
88
|
|
117
|
-
dimension =
|
89
|
+
dimension = PAPER_FORMATS.fetch(format)
|
118
90
|
options.merge!(paper_width: dimension[:width],
|
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
@@ -9,6 +9,9 @@ require "ferrum/dialog"
|
|
9
9
|
require "ferrum/network"
|
10
10
|
require "ferrum/page/frames"
|
11
11
|
require "ferrum/page/screenshot"
|
12
|
+
require "ferrum/page/animation"
|
13
|
+
require "ferrum/page/tracing"
|
14
|
+
require "ferrum/page/stream"
|
12
15
|
require "ferrum/browser/client"
|
13
16
|
|
14
17
|
module Ferrum
|
@@ -31,21 +34,26 @@ module Ferrum
|
|
31
34
|
|
32
35
|
extend Forwardable
|
33
36
|
delegate %i[at_css at_xpath css xpath
|
34
|
-
current_url current_title url title body doctype
|
37
|
+
current_url current_title url title body doctype content=
|
35
38
|
execution_id evaluate evaluate_on evaluate_async execute evaluate_func
|
36
39
|
add_script_tag add_style_tag] => :main_frame
|
37
40
|
|
38
|
-
include
|
41
|
+
include Animation
|
42
|
+
include Screenshot
|
43
|
+
include Frames
|
44
|
+
include Stream
|
39
45
|
|
40
46
|
attr_accessor :referrer
|
41
47
|
attr_reader :target_id, :browser,
|
42
48
|
:headers, :cookies, :network,
|
43
|
-
:mouse, :keyboard, :event,
|
49
|
+
:mouse, :keyboard, :event,
|
50
|
+
:tracing
|
44
51
|
|
45
52
|
def initialize(target_id, browser)
|
46
53
|
@frames = {}
|
47
54
|
@main_frame = Frame.new(nil, self)
|
48
|
-
@
|
55
|
+
@browser = browser
|
56
|
+
@target_id = target_id
|
49
57
|
@event = Event.new.tap(&:set)
|
50
58
|
|
51
59
|
host = @browser.process.host
|
@@ -53,9 +61,12 @@ module Ferrum
|
|
53
61
|
ws_url = "ws://#{host}:#{port}/devtools/page/#{@target_id}"
|
54
62
|
@client = Browser::Client.new(browser, ws_url, id_starts_with: 1000)
|
55
63
|
|
56
|
-
@mouse
|
57
|
-
@
|
64
|
+
@mouse = Mouse.new(self)
|
65
|
+
@keyboard = Keyboard.new(self)
|
66
|
+
@headers = Headers.new(self)
|
67
|
+
@cookies = Cookies.new(self)
|
58
68
|
@network = Network.new(self)
|
69
|
+
@tracing = Tracing.new(self)
|
59
70
|
|
60
71
|
subscribe
|
61
72
|
prepare_page
|
@@ -65,6 +76,10 @@ module Ferrum
|
|
65
76
|
@browser.timeout
|
66
77
|
end
|
67
78
|
|
79
|
+
def context
|
80
|
+
@browser.contexts.find_by(target_id: target_id)
|
81
|
+
end
|
82
|
+
|
68
83
|
def go_to(url = nil)
|
69
84
|
options = { url: combine_url!(url) }
|
70
85
|
options.merge!(referrer: referrer) if referrer
|
@@ -76,6 +91,7 @@ module Ferrum
|
|
76
91
|
net::ERR_CONNECTION_TIMED_OUT].include?(response["errorText"])
|
77
92
|
raise StatusError, options[:url]
|
78
93
|
end
|
94
|
+
|
79
95
|
response["frameId"]
|
80
96
|
rescue TimeoutError
|
81
97
|
if @browser.pending_connection_errors
|
@@ -84,6 +100,7 @@ module Ferrum
|
|
84
100
|
end
|
85
101
|
end
|
86
102
|
alias goto go_to
|
103
|
+
alias go go_to
|
87
104
|
|
88
105
|
def close
|
89
106
|
@headers.clear
|
@@ -108,10 +125,20 @@ module Ferrum
|
|
108
125
|
fitWindow: false)
|
109
126
|
end
|
110
127
|
|
128
|
+
def position
|
129
|
+
@browser.command("Browser.getWindowBounds", windowId: window_id).fetch("bounds").values_at("left", "top")
|
130
|
+
end
|
131
|
+
|
132
|
+
def position=(options)
|
133
|
+
@browser.command("Browser.setWindowBounds",
|
134
|
+
windowId: window_id,
|
135
|
+
bounds: { left: options[:left], top: options[:top] })
|
136
|
+
end
|
137
|
+
|
111
138
|
def refresh
|
112
139
|
command("Page.reload", wait: timeout, slowmoable: true)
|
113
140
|
end
|
114
|
-
|
141
|
+
alias reload refresh
|
115
142
|
|
116
143
|
def stop
|
117
144
|
command("Page.stopLoading", slowmoable: true)
|
@@ -131,8 +158,7 @@ module Ferrum
|
|
131
158
|
@event.set
|
132
159
|
end
|
133
160
|
|
134
|
-
def bypass_csp(
|
135
|
-
enabled = !!value
|
161
|
+
def bypass_csp(enabled: true)
|
136
162
|
command("Page.setBypassCSP", enabled: enabled)
|
137
163
|
enabled
|
138
164
|
end
|
@@ -146,14 +172,15 @@ module Ferrum
|
|
146
172
|
end
|
147
173
|
|
148
174
|
def command(method, wait: 0, slowmoable: false, **params)
|
149
|
-
iteration = @event.reset if wait
|
150
|
-
sleep(@browser.slowmo) if slowmoable && @browser.slowmo
|
175
|
+
iteration = @event.reset if wait.positive?
|
176
|
+
sleep(@browser.slowmo) if slowmoable && @browser.slowmo.positive?
|
151
177
|
result = @client.command(method, params)
|
152
178
|
|
153
|
-
if wait
|
154
|
-
@event.wait(wait)
|
155
|
-
|
156
|
-
|
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.
|
157
184
|
if iteration != @event.iteration
|
158
185
|
set = @event.wait(@browser.timeout)
|
159
186
|
raise TimeoutError unless set
|
@@ -205,8 +232,19 @@ module Ferrum
|
|
205
232
|
|
206
233
|
if @browser.js_errors
|
207
234
|
on("Runtime.exceptionThrown") do |params|
|
208
|
-
# FIXME https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/
|
209
|
-
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
|
210
248
|
end
|
211
249
|
end
|
212
250
|
end
|
@@ -219,8 +257,22 @@ module Ferrum
|
|
219
257
|
command("Log.enable")
|
220
258
|
command("Network.enable")
|
221
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
|
+
|
222
267
|
if @browser.options[:save_path]
|
223
|
-
|
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)
|
224
276
|
end
|
225
277
|
|
226
278
|
@browser.extensions.each do |extension|
|
@@ -233,14 +285,14 @@ module Ferrum
|
|
233
285
|
resize(width: width, height: height)
|
234
286
|
|
235
287
|
response = command("Page.getNavigationHistory")
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
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
|
244
296
|
end
|
245
297
|
|
246
298
|
def inject_extensions
|
@@ -259,13 +311,15 @@ module Ferrum
|
|
259
311
|
def history_navigate(delta:)
|
260
312
|
history = command("Page.getNavigationHistory")
|
261
313
|
index, entries = history.values_at("currentIndex", "entries")
|
314
|
+
entry = entries[index + delta]
|
262
315
|
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
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"])
|
269
323
|
end
|
270
324
|
|
271
325
|
def combine_url!(url_or_path)
|
@@ -279,8 +333,8 @@ module Ferrum
|
|
279
333
|
(nil_or_relative ? @browser.base_url.join(url.to_s) : url).to_s
|
280
334
|
end
|
281
335
|
|
282
|
-
def
|
283
|
-
|
336
|
+
def document_node_id
|
337
|
+
command("DOM.getDocument", depth: 0).dig("root", "nodeId")
|
284
338
|
end
|
285
339
|
end
|
286
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
|