ferrum 0.10.1 → 0.12
Sign up to get free protection for your applications and to get access to all the features.
- 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
|