ferrum 0.11 → 0.13
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 +174 -30
- data/lib/ferrum/browser/binary.rb +46 -0
- data/lib/ferrum/browser/client.rb +17 -16
- data/lib/ferrum/browser/command.rb +10 -12
- data/lib/ferrum/browser/options/base.rb +2 -11
- data/lib/ferrum/browser/options/chrome.rb +29 -18
- data/lib/ferrum/browser/options/firefox.rb +13 -9
- data/lib/ferrum/browser/options.rb +84 -0
- data/lib/ferrum/browser/process.rb +45 -40
- data/lib/ferrum/browser/subscriber.rb +1 -3
- data/lib/ferrum/browser/version_info.rb +71 -0
- data/lib/ferrum/browser/web_socket.rb +9 -12
- data/lib/ferrum/browser/xvfb.rb +4 -8
- data/lib/ferrum/browser.rb +193 -47
- data/lib/ferrum/context.rb +9 -4
- data/lib/ferrum/contexts.rb +12 -10
- data/lib/ferrum/cookies/cookie.rb +126 -0
- data/lib/ferrum/cookies.rb +93 -55
- data/lib/ferrum/dialog.rb +30 -0
- data/lib/ferrum/errors.rb +115 -0
- data/lib/ferrum/frame/dom.rb +177 -0
- data/lib/ferrum/frame/runtime.rb +58 -75
- data/lib/ferrum/frame.rb +118 -23
- data/lib/ferrum/headers.rb +30 -2
- data/lib/ferrum/keyboard.rb +56 -13
- data/lib/ferrum/mouse.rb +92 -7
- data/lib/ferrum/network/auth_request.rb +7 -2
- data/lib/ferrum/network/exchange.rb +97 -12
- data/lib/ferrum/network/intercepted_request.rb +10 -8
- data/lib/ferrum/network/request.rb +69 -0
- data/lib/ferrum/network/response.rb +85 -3
- data/lib/ferrum/network.rb +285 -36
- data/lib/ferrum/node.rb +69 -23
- data/lib/ferrum/page/animation.rb +16 -1
- data/lib/ferrum/page/frames.rb +111 -30
- data/lib/ferrum/page/screenshot.rb +142 -65
- data/lib/ferrum/page/stream.rb +38 -0
- data/lib/ferrum/page/tracing.rb +97 -0
- data/lib/ferrum/page.rb +224 -60
- data/lib/ferrum/proxy.rb +147 -0
- data/lib/ferrum/{rbga.rb → rgba.rb} +4 -2
- data/lib/ferrum/target.rb +7 -4
- 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 +63 -51
data/lib/ferrum/page/frames.rb
CHANGED
@@ -5,41 +5,124 @@ require "ferrum/frame"
|
|
5
5
|
module Ferrum
|
6
6
|
class Page
|
7
7
|
module Frames
|
8
|
+
# The page's main frame, the top of the tree and the parent of all frames.
|
9
|
+
#
|
10
|
+
# @return [Frame]
|
8
11
|
attr_reader :main_frame
|
9
12
|
|
13
|
+
#
|
14
|
+
# Returns all the frames current page have.
|
15
|
+
#
|
16
|
+
# @return [Array<Frame>]
|
17
|
+
#
|
18
|
+
# @example
|
19
|
+
# browser.go_to("https://www.w3schools.com/tags/tag_frame.asp")
|
20
|
+
# browser.frames # =>
|
21
|
+
# # [
|
22
|
+
# # #<Ferrum::Frame
|
23
|
+
# # @id="C6D104CE454A025FBCF22B98DE612B12"
|
24
|
+
# # @parent_id=nil @name=nil @state=:stopped_loading @execution_id=1>,
|
25
|
+
# # #<Ferrum::Frame
|
26
|
+
# # @id="C09C4E4404314AAEAE85928EAC109A93"
|
27
|
+
# # @parent_id="C6D104CE454A025FBCF22B98DE612B12" @state=:stopped_loading @execution_id=2>,
|
28
|
+
# # #<Ferrum::Frame
|
29
|
+
# # @id="2E9C7F476ED09D87A42F2FEE3C6FBC3C"
|
30
|
+
# # @parent_id="C6D104CE454A025FBCF22B98DE612B12" @state=:stopped_loading @execution_id=3>,
|
31
|
+
# # ...
|
32
|
+
# # ]
|
33
|
+
#
|
10
34
|
def frames
|
11
35
|
@frames.values
|
12
36
|
end
|
13
37
|
|
14
|
-
|
38
|
+
#
|
39
|
+
# Find frame by given options.
|
40
|
+
#
|
41
|
+
# @param [String] id
|
42
|
+
# Unique frame's id that browser provides.
|
43
|
+
#
|
44
|
+
# @param [String] name
|
45
|
+
# Frame's name if there's one.
|
46
|
+
#
|
47
|
+
# @param [String] execution_id
|
48
|
+
# Frame's context execution id.
|
49
|
+
#
|
50
|
+
# @return [Frame, nil]
|
51
|
+
# The matching frame.
|
52
|
+
#
|
53
|
+
# @example
|
54
|
+
# browser.frame_by(id: "C6D104CE454A025FBCF22B98DE612B12")
|
55
|
+
#
|
56
|
+
def frame_by(id: nil, name: nil, execution_id: nil)
|
15
57
|
if id
|
16
58
|
@frames[id]
|
17
59
|
elsif name
|
18
60
|
frames.find { |f| f.name == name }
|
61
|
+
elsif execution_id
|
62
|
+
frames.find { |f| f.execution_id == execution_id }
|
19
63
|
else
|
20
64
|
raise ArgumentError
|
21
65
|
end
|
22
66
|
end
|
23
67
|
|
24
68
|
def frames_subscribe
|
69
|
+
subscribe_frame_attached
|
70
|
+
subscribe_frame_detached
|
71
|
+
subscribe_frame_started_loading
|
72
|
+
subscribe_frame_navigated
|
73
|
+
subscribe_frame_stopped_loading
|
74
|
+
|
75
|
+
subscribe_navigated_within_document
|
76
|
+
|
77
|
+
subscribe_request_will_be_sent
|
78
|
+
|
79
|
+
subscribe_execution_context_created
|
80
|
+
subscribe_execution_context_destroyed
|
81
|
+
subscribe_execution_contexts_cleared
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def subscribe_frame_attached
|
25
87
|
on("Page.frameAttached") do |params|
|
26
88
|
parent_frame_id, frame_id = params.values_at("parentFrameId", "frameId")
|
27
|
-
@frames
|
89
|
+
@frames.put_if_absent(frame_id, Frame.new(frame_id, self, parent_frame_id))
|
28
90
|
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def subscribe_frame_detached
|
94
|
+
on("Page.frameDetached") do |params|
|
95
|
+
frame = @frames[params["frameId"]]
|
96
|
+
|
97
|
+
if frame&.main?
|
98
|
+
frame.execution_id = nil
|
99
|
+
else
|
100
|
+
@frames.delete(params["frameId"])
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
29
104
|
|
105
|
+
def subscribe_frame_started_loading
|
30
106
|
on("Page.frameStartedLoading") do |params|
|
31
107
|
frame = @frames[params["frameId"]]
|
32
|
-
frame.state = :started_loading
|
108
|
+
frame.state = :started_loading if frame
|
33
109
|
@event.reset
|
34
110
|
end
|
111
|
+
end
|
35
112
|
|
113
|
+
def subscribe_frame_navigated
|
36
114
|
on("Page.frameNavigated") do |params|
|
37
115
|
frame_id, name = params["frame"]&.values_at("id", "name")
|
38
116
|
frame = @frames[frame_id]
|
39
|
-
|
40
|
-
frame
|
117
|
+
|
118
|
+
if frame
|
119
|
+
frame.state = :navigated
|
120
|
+
frame.name = name
|
121
|
+
end
|
41
122
|
end
|
123
|
+
end
|
42
124
|
|
125
|
+
def subscribe_frame_stopped_loading
|
43
126
|
on("Page.frameStoppedLoading") do |params|
|
44
127
|
# `DOM.performSearch` doesn't work without getting #document node first.
|
45
128
|
# It returns node with nodeId 1 and nodeType 9 from which descend the
|
@@ -47,7 +130,7 @@ module Ferrum
|
|
47
130
|
# node will change the id and all subsequent nodes have to change id too.
|
48
131
|
if @main_frame.id == params["frameId"]
|
49
132
|
@event.set if idling?
|
50
|
-
|
133
|
+
document_node_id
|
51
134
|
end
|
52
135
|
|
53
136
|
frame = @frames[params["frameId"]]
|
@@ -55,60 +138,58 @@ module Ferrum
|
|
55
138
|
|
56
139
|
@event.set if idling?
|
57
140
|
end
|
141
|
+
end
|
58
142
|
|
143
|
+
def subscribe_navigated_within_document
|
59
144
|
on("Page.navigatedWithinDocument") do
|
60
145
|
@event.set if idling?
|
61
146
|
end
|
147
|
+
end
|
62
148
|
|
149
|
+
def subscribe_request_will_be_sent
|
63
150
|
on("Network.requestWillBeSent") do |params|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
@event.reset if params["type"] == "Document"
|
70
|
-
end
|
151
|
+
# Possible types:
|
152
|
+
# Document, Stylesheet, Image, Media, Font, Script, TextTrack, XHR,
|
153
|
+
# Fetch, EventSource, WebSocket, Manifest, SignedExchange, Ping,
|
154
|
+
# CSPViolationReport, Other
|
155
|
+
@event.reset if params["frameId"] == @main_frame.id && params["type"] == "Document"
|
71
156
|
end
|
157
|
+
end
|
72
158
|
|
159
|
+
def subscribe_execution_context_created
|
73
160
|
on("Runtime.executionContextCreated") do |params|
|
74
|
-
setting_up_main_frame = false
|
75
161
|
context_id = params.dig("context", "id")
|
76
162
|
frame_id = params.dig("context", "auxData", "frameId")
|
77
163
|
|
78
164
|
unless @main_frame.id
|
79
165
|
root_frame = command("Page.getFrameTree").dig("frameTree", "frame", "id")
|
80
166
|
if frame_id == root_frame
|
81
|
-
setting_up_main_frame = true
|
82
167
|
@main_frame.id = frame_id
|
83
|
-
@frames
|
168
|
+
@frames.put_if_absent(frame_id, @main_frame)
|
84
169
|
end
|
85
170
|
end
|
86
171
|
|
87
|
-
frame = @frames
|
88
|
-
frame.
|
89
|
-
|
90
|
-
# Set event because `execution_id` might raise NoExecutionContextError
|
91
|
-
@event.set if setting_up_main_frame
|
92
|
-
|
93
|
-
@frames[frame_id] ||= frame
|
172
|
+
frame = @frames.fetch_or_store(frame_id, Frame.new(frame_id, self))
|
173
|
+
frame.execution_id = context_id
|
94
174
|
end
|
175
|
+
end
|
95
176
|
|
177
|
+
def subscribe_execution_context_destroyed
|
96
178
|
on("Runtime.executionContextDestroyed") do |params|
|
97
179
|
execution_id = params["executionContextId"]
|
98
|
-
frame =
|
99
|
-
frame
|
180
|
+
frame = frame_by(execution_id: execution_id)
|
181
|
+
frame&.execution_id = nil
|
100
182
|
end
|
183
|
+
end
|
101
184
|
|
185
|
+
def subscribe_execution_contexts_cleared
|
102
186
|
on("Runtime.executionContextsCleared") do
|
103
|
-
@frames.
|
104
|
-
@main_frame.reset_execution_id
|
187
|
+
@frames.each_value { |f| f.execution_id = nil }
|
105
188
|
end
|
106
189
|
end
|
107
190
|
|
108
|
-
private
|
109
|
-
|
110
191
|
def idling?
|
111
|
-
@frames.all? { |
|
192
|
+
@frames.values.all? { |f| f.state == :stopped_loading }
|
112
193
|
end
|
113
194
|
end
|
114
195
|
end
|
@@ -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,64 @@ 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
|
-
|
30
|
-
|
29
|
+
#
|
30
|
+
# Saves screenshot on a disk or returns it as base64.
|
31
|
+
#
|
32
|
+
# @param [Hash{Symbol => Object}] opts
|
33
|
+
#
|
34
|
+
# @option opts [String] :path
|
35
|
+
# The path to save a screenshot on the disk. `:encoding` will be set to
|
36
|
+
# `:binary` automatically.
|
37
|
+
#
|
38
|
+
# @option opts [:base64, :binary] :encoding
|
39
|
+
# The encoding the image should be returned in.
|
40
|
+
#
|
41
|
+
# @option opts ["jpeg", "png"] :format
|
42
|
+
# The format the image should be returned in.
|
43
|
+
#
|
44
|
+
# @option opts [Integer] :quality
|
45
|
+
# The image quality. **Note:** 0-100 works for jpeg only.
|
46
|
+
#
|
47
|
+
# @option opts [Boolean] :full
|
48
|
+
# Whether you need full page screenshot or a viewport.
|
49
|
+
#
|
50
|
+
# @option opts [String] :selector
|
51
|
+
# CSS selector for the given element.
|
52
|
+
#
|
53
|
+
# @option opts [Float] :scale
|
54
|
+
# Zoom in/out.
|
55
|
+
#
|
56
|
+
# @option opts [Ferrum::RGBA] :background_color
|
57
|
+
# Sets the background color.
|
58
|
+
#
|
59
|
+
# @example
|
60
|
+
# browser.go_to("https://google.com/")
|
61
|
+
#
|
62
|
+
# @example Save on the disk in PNG:
|
63
|
+
# browser.screenshot(path: "google.png") # => 134660
|
64
|
+
#
|
65
|
+
# @example Save on the disk in JPG:
|
66
|
+
# browser.screenshot(path: "google.jpg") # => 30902
|
67
|
+
#
|
68
|
+
# @example Save to Base64 the whole page not only viewport and reduce quality:
|
69
|
+
# browser.screenshot(full: true, quality: 60) # "iVBORw0KGgoAAAANS...
|
70
|
+
#
|
71
|
+
# @example Save with specific background color:
|
72
|
+
# browser.screenshot(background_color: Ferrum::RGBA.new(0, 0, 0, 0.0))
|
73
|
+
#
|
31
74
|
def screenshot(**opts)
|
32
75
|
path, encoding = common_options(**opts)
|
33
76
|
options = screenshot_options(path, **opts)
|
@@ -38,21 +81,63 @@ module Ferrum
|
|
38
81
|
save_file(path, bin)
|
39
82
|
end
|
40
83
|
|
84
|
+
#
|
85
|
+
# Saves PDF on a disk or returns it as Base64.
|
86
|
+
#
|
87
|
+
# @param [Hash{Symbol => Object}] opts
|
88
|
+
#
|
89
|
+
# @option opts [String] :path
|
90
|
+
# The path to save a screenshot on the disk. `:encoding` will be set to
|
91
|
+
# `:binary` automatically.
|
92
|
+
#
|
93
|
+
# @option opts [:base64, :binary] :encoding
|
94
|
+
# The encoding the image should be returned in.
|
95
|
+
#
|
96
|
+
# @option opts [Boolean] :landscape (false)
|
97
|
+
# Page orientation.
|
98
|
+
#
|
99
|
+
# @option opts [Float] :scale
|
100
|
+
# Zoom in/out.
|
101
|
+
#
|
102
|
+
# @option opts [:letter, :legal, :tabloid, :ledger, :A0, :A1, :A2, :A3, :A4, :A5, :A6] :format
|
103
|
+
# The standard paper size.
|
104
|
+
#
|
105
|
+
# @option opts [Float] :paper_width
|
106
|
+
# Sets the paper's width.
|
107
|
+
#
|
108
|
+
# @option opts [Float] :paper_height
|
109
|
+
# Sets the paper's height.
|
110
|
+
#
|
111
|
+
# @note
|
112
|
+
# See other [native options](https://chromedevtools.github.io/devtools-protocol/tot/Page#method-printToPDF) you
|
113
|
+
# can pass.
|
114
|
+
#
|
115
|
+
# @example
|
116
|
+
# browser.go_to("https://google.com/")
|
117
|
+
# # Save to disk as a PDF
|
118
|
+
# browser.pdf(path: "google.pdf", paper_width: 1.0, paper_height: 1.0) # => true
|
119
|
+
#
|
41
120
|
def pdf(**opts)
|
42
121
|
path, encoding = common_options(**opts)
|
43
122
|
options = pdf_options(**opts).merge(transferMode: "ReturnAsStream")
|
44
123
|
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
|
124
|
+
stream_to(path: path, encoding: encoding, handle: handle)
|
51
125
|
end
|
52
126
|
|
127
|
+
#
|
128
|
+
# Saves MHTML on a disk or returns it as a string.
|
129
|
+
#
|
130
|
+
# @param [String, nil] path
|
131
|
+
# The path to save a file on the disk.
|
132
|
+
#
|
133
|
+
# @example
|
134
|
+
# browser.go_to("https://google.com/")
|
135
|
+
# browser.mhtml(path: "google.mhtml") # => 87742
|
136
|
+
#
|
53
137
|
def mhtml(path: nil)
|
54
138
|
data = command("Page.captureSnapshot", format: :mhtml).fetch("data")
|
55
139
|
return data if path.nil?
|
140
|
+
|
56
141
|
save_file(path, data)
|
57
142
|
end
|
58
143
|
|
@@ -73,30 +158,8 @@ module Ferrum
|
|
73
158
|
|
74
159
|
def save_file(path, data)
|
75
160
|
return data unless path
|
76
|
-
File.open(path.to_s, "wb") { |f| f.write(data) }
|
77
|
-
end
|
78
161
|
|
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
|
162
|
+
File.binwrite(path.to_s, data)
|
100
163
|
end
|
101
164
|
|
102
165
|
def common_options(encoding: :base64, path: nil, **_)
|
@@ -119,44 +182,57 @@ module Ferrum
|
|
119
182
|
paper_height: dimension[:height])
|
120
183
|
end
|
121
184
|
|
122
|
-
options.
|
185
|
+
options.transform_keys { |k| to_camel_case(k) }
|
123
186
|
end
|
124
187
|
|
125
|
-
def screenshot_options(path = nil, format: nil, scale: 1.0, **
|
126
|
-
|
188
|
+
def screenshot_options(path = nil, format: nil, scale: 1.0, **options)
|
189
|
+
screenshot_options = {}
|
190
|
+
|
191
|
+
format, quality = format_options(format, path, options[:quality])
|
192
|
+
screenshot_options.merge!(quality: quality) if quality
|
193
|
+
screenshot_options.merge!(format: format)
|
194
|
+
|
195
|
+
clip = area_options(options[:full], options[:selector], scale)
|
196
|
+
screenshot_options.merge!(clip: clip) if clip
|
197
|
+
|
198
|
+
screenshot_options
|
199
|
+
end
|
127
200
|
|
201
|
+
def format_options(format, path, quality)
|
128
202
|
format ||= path ? File.extname(path).delete(".") : "png"
|
129
203
|
format = "jpeg" if format == "jpg"
|
130
204
|
raise "Not supported options `:format` #{format}. jpeg | png" if format !~ /jpeg|png/i
|
131
|
-
options.merge!(format: format)
|
132
205
|
|
133
|
-
|
206
|
+
quality ||= 75 if format == "jpeg"
|
134
207
|
|
135
|
-
|
136
|
-
|
137
|
-
end
|
208
|
+
[format, quality]
|
209
|
+
end
|
138
210
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
elsif opts[:selector]
|
143
|
-
options.merge!(clip: get_bounding_rect(opts[:selector]).merge(scale: scale))
|
144
|
-
end
|
211
|
+
def area_options(full, selector, scale)
|
212
|
+
message = "Ignoring :selector in #screenshot since full: true was given at #{caller(1..1).first}"
|
213
|
+
warn(message) if full && selector
|
145
214
|
|
146
|
-
|
147
|
-
|
215
|
+
clip = if full
|
216
|
+
width, height = document_size
|
217
|
+
{ x: 0, y: 0, width: width, height: height, scale: scale } if width.positive? && height.positive?
|
218
|
+
elsif selector
|
219
|
+
bounding_rect(selector).merge(scale: scale)
|
220
|
+
end
|
221
|
+
|
222
|
+
if scale != 1
|
223
|
+
unless clip
|
148
224
|
width, height = viewport_size
|
149
|
-
|
225
|
+
clip = { x: 0, y: 0, width: width, height: height }
|
150
226
|
end
|
151
227
|
|
152
|
-
|
228
|
+
clip.merge!(scale: scale)
|
153
229
|
end
|
154
230
|
|
155
|
-
|
231
|
+
clip
|
156
232
|
end
|
157
233
|
|
158
|
-
def
|
159
|
-
rect = evaluate_async(%
|
234
|
+
def bounding_rect(selector)
|
235
|
+
rect = evaluate_async(%(
|
160
236
|
const rect = document
|
161
237
|
.querySelector('#{selector}')
|
162
238
|
.getBoundingClientRect();
|
@@ -169,7 +245,8 @@ module Ferrum
|
|
169
245
|
|
170
246
|
def to_camel_case(option)
|
171
247
|
return :preferCSSPageSize if option == :prefer_css_page_size
|
172
|
-
|
248
|
+
|
249
|
+
option.to_s.gsub(%r{(?:_|(/))([a-z\d]*)}) { "#{Regexp.last_match(1)}#{Regexp.last_match(2).capitalize}" }.to_sym
|
173
250
|
end
|
174
251
|
|
175
252
|
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,97 @@
|
|
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
|
+
#
|
23
|
+
# Accepts block, records trace and by default returns trace data from `Tracing.tracingComplete` event as output.
|
24
|
+
#
|
25
|
+
# @param [String, nil] path
|
26
|
+
# Save data on the disk.
|
27
|
+
#
|
28
|
+
# @param [:binary, :base64] encoding
|
29
|
+
# Encode output as Base64 or plain text.
|
30
|
+
#
|
31
|
+
# @param [Float, nil] timeout
|
32
|
+
# Wait until file streaming finishes in the specified time or raise
|
33
|
+
# error.
|
34
|
+
#
|
35
|
+
# @param [Boolean] screenshots
|
36
|
+
# capture screenshots in the trace.
|
37
|
+
#
|
38
|
+
# @param [Hash{String => Object}] trace_config
|
39
|
+
# config for [trace](https://chromedevtools.github.io/devtools-protocol/tot/Tracing/#type-TraceConfig),
|
40
|
+
# for categories see [getCategories](https://chromedevtools.github.io/devtools-protocol/tot/Tracing/#method-getCategories),
|
41
|
+
# only one trace config can be active at a time per browser.
|
42
|
+
#
|
43
|
+
# @return [String, true]
|
44
|
+
# The trace data from the `Tracing.tracingComplete` event.
|
45
|
+
# When `path` is specified returns `true` and stores trace data into
|
46
|
+
# file.
|
47
|
+
#
|
48
|
+
def record(path: nil, encoding: :binary, timeout: nil, trace_config: nil, screenshots: false)
|
49
|
+
@path = path
|
50
|
+
@encoding = encoding
|
51
|
+
@pending = Concurrent::IVar.new
|
52
|
+
trace_config ||= DEFAULT_TRACE_CONFIG.dup
|
53
|
+
|
54
|
+
if screenshots
|
55
|
+
included = trace_config.fetch(:includedCategories, [])
|
56
|
+
trace_config.merge!(includedCategories: included | SCREENSHOT_CATEGORIES)
|
57
|
+
end
|
58
|
+
|
59
|
+
subscribe_tracing_complete
|
60
|
+
|
61
|
+
start(trace_config)
|
62
|
+
yield
|
63
|
+
stop
|
64
|
+
|
65
|
+
@pending.value!(timeout || @page.timeout)
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def start(config)
|
71
|
+
@page.command("Tracing.start", transferMode: "ReturnAsStream", traceConfig: config)
|
72
|
+
end
|
73
|
+
|
74
|
+
def stop
|
75
|
+
@page.command("Tracing.end")
|
76
|
+
end
|
77
|
+
|
78
|
+
def subscribe_tracing_complete
|
79
|
+
return if @subscribed_tracing_complete
|
80
|
+
|
81
|
+
@page.on("Tracing.tracingComplete") do |event, index|
|
82
|
+
next if index.to_i != 0
|
83
|
+
|
84
|
+
@pending.set(stream_handle(event["stream"]))
|
85
|
+
rescue StandardError => e
|
86
|
+
@pending.fail(e)
|
87
|
+
end
|
88
|
+
|
89
|
+
@subscribed_tracing_complete = true
|
90
|
+
end
|
91
|
+
|
92
|
+
def stream_handle(handle)
|
93
|
+
@page.stream_to(path: @path, encoding: @encoding, handle: handle)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|