ferrum 0.5 → 0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +37 -12
- data/lib/ferrum/browser.rb +3 -2
- data/lib/ferrum/frame.rb +52 -0
- data/lib/ferrum/{page → frame}/dom.rb +16 -11
- data/lib/ferrum/{page → frame}/runtime.rb +68 -11
- data/lib/ferrum/network.rb +14 -9
- data/lib/ferrum/network/auth_request.rb +62 -0
- data/lib/ferrum/network/exchange.rb +7 -2
- data/lib/ferrum/network/intercepted_request.rb +31 -10
- data/lib/ferrum/network/response.rb +25 -3
- data/lib/ferrum/node.rb +11 -2
- data/lib/ferrum/page.rb +30 -31
- data/lib/ferrum/page/frames.rb +115 -0
- data/lib/ferrum/page/screenshot.rb +72 -10
- data/lib/ferrum/target.rb +4 -0
- data/lib/ferrum/version.rb +1 -1
- metadata +7 -5
- data/lib/ferrum/page/frame.rb +0 -135
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4f657c088b22a9ff5809b6ecd0d7005cd0c2252645b262e76faee1b6e3af94c8
|
4
|
+
data.tar.gz: d22842188425c753e0a0e113be52e0176e8a7e1a97211c44bf3016e90aa0d1d4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a519e80c6b1450ba85c555d4c5eb547ffb62f129e7873515ba2f5bad4726548289bcc3b4db95932873620e4b645f2f9b7f12c8bd0fdbe0ddada4f0faae080720
|
7
|
+
data.tar.gz: 91a8f95433f9630dd0f2b36c42ea14bf28b49ee9504738c664399eeb7fc3d192bd26c14ae7de707dcaa8273ec65d7b1a8e9bcaeeff4287392a5e1b312ea03b0d
|
data/README.md
CHANGED
@@ -221,20 +221,20 @@ browser.xpath("//a[@aria-label='Issues you created']") # => [Node]
|
|
221
221
|
|
222
222
|
#### current_url : `String`
|
223
223
|
|
224
|
-
Returns current window location href.
|
224
|
+
Returns current top window location href.
|
225
225
|
|
226
226
|
```ruby
|
227
227
|
browser.goto("https://google.com/")
|
228
228
|
browser.current_url # => "https://www.google.com/"
|
229
229
|
```
|
230
230
|
|
231
|
-
####
|
231
|
+
#### current_title : `String`
|
232
232
|
|
233
|
-
Returns current window title
|
233
|
+
Returns current top window title
|
234
234
|
|
235
235
|
```ruby
|
236
236
|
browser.goto("https://google.com/")
|
237
|
-
browser.
|
237
|
+
browser.current_title # => "Google"
|
238
238
|
```
|
239
239
|
|
240
240
|
#### body : `String`
|
@@ -285,6 +285,8 @@ Saves PDF on a disk or returns it as base64.
|
|
285
285
|
Base64
|
286
286
|
* :landscape `Boolean` paper orientation. Defaults to false.
|
287
287
|
* :scale `Float` zoom in/out
|
288
|
+
* :format `symbol` standard paper sizes :letter, :legal, :tabloid, :ledger, :A0, :A1, :A2, :A3, :A4, :A5, :A6
|
289
|
+
|
288
290
|
* :paper_width `Float` set paper width
|
289
291
|
* :paper_height `Float` set paper height
|
290
292
|
* See other [native options](https://chromedevtools.github.io/devtools-protocol/tot/Page#method-printToPDF) you can pass
|
@@ -368,6 +370,8 @@ browser.network.intercept
|
|
368
370
|
browser.on(:request) do |request|
|
369
371
|
if request.match?(/bla-bla/)
|
370
372
|
request.abort
|
373
|
+
elsif request.match?(/lorem/)
|
374
|
+
request.respond(body: "Lorem ipsum")
|
371
375
|
else
|
372
376
|
request.continue
|
373
377
|
end
|
@@ -601,22 +605,43 @@ simple value.
|
|
601
605
|
browser.execute(%(1 + 1)) # => true
|
602
606
|
```
|
603
607
|
|
608
|
+
#### add_script_tag(\*\*options) : `Boolean`
|
609
|
+
|
610
|
+
* options `Hash`
|
611
|
+
* :url `String`
|
612
|
+
* :path `String`
|
613
|
+
* :content `String`
|
614
|
+
* :type `String` - `text/javascript` by default
|
615
|
+
|
616
|
+
```ruby
|
617
|
+
browser.add_script_tag(url: "http://example.com/stylesheet.css") # => true
|
618
|
+
```
|
619
|
+
|
620
|
+
#### add_style_tag(\*\*options) : `Boolean`
|
621
|
+
|
622
|
+
* options `Hash`
|
623
|
+
* :url `String`
|
624
|
+
* :path `String`
|
625
|
+
* :content `String`
|
626
|
+
|
627
|
+
```ruby
|
628
|
+
browser.add_style_tag(content: "h1 { font-size: 40px; }") # => true
|
629
|
+
```
|
630
|
+
|
604
631
|
|
605
632
|
## Frames
|
606
633
|
|
607
|
-
####
|
608
|
-
####
|
609
|
-
####
|
634
|
+
#### frames
|
635
|
+
#### main_frame
|
636
|
+
#### frame_by
|
610
637
|
|
611
638
|
Play around inside given frame
|
612
639
|
|
613
640
|
```ruby
|
614
641
|
browser.goto("https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe")
|
615
|
-
frame = browser.
|
616
|
-
|
617
|
-
|
618
|
-
puts browser.frame_url # => https://interactive-examples.mdn.mozilla.net/pages/tabbed/iframe.html
|
619
|
-
end
|
642
|
+
frame = browser.frames[1]
|
643
|
+
puts frame.title # => HTML Demo: <iframe>
|
644
|
+
puts frame.url # => https://interactive-examples.mdn.mozilla.net/pages/tabbed/iframe.html
|
620
645
|
```
|
621
646
|
|
622
647
|
|
data/lib/ferrum/browser.rb
CHANGED
@@ -17,12 +17,13 @@ module Ferrum
|
|
17
17
|
delegate %i[default_context] => :contexts
|
18
18
|
delegate %i[targets create_target create_page page pages windows] => :default_context
|
19
19
|
delegate %i[goto back forward refresh
|
20
|
-
at_css at_xpath css xpath current_url title body
|
20
|
+
at_css at_xpath css xpath current_url title body doctype
|
21
21
|
headers cookies network
|
22
22
|
mouse keyboard
|
23
23
|
screenshot pdf viewport_size
|
24
|
+
frames frame_by main_frame
|
24
25
|
evaluate evaluate_on evaluate_async execute
|
25
|
-
|
26
|
+
add_script_tag add_style_tag
|
26
27
|
on] => :page
|
27
28
|
|
28
29
|
attr_reader :client, :process, :contexts, :logger, :js_errors,
|
data/lib/ferrum/frame.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ferrum/frame/dom"
|
4
|
+
require "ferrum/frame/runtime"
|
5
|
+
|
6
|
+
module Ferrum
|
7
|
+
class Frame
|
8
|
+
include DOM, Runtime
|
9
|
+
|
10
|
+
attr_reader :id, :page, :parent_id, :state
|
11
|
+
attr_writer :execution_id
|
12
|
+
attr_accessor :name
|
13
|
+
|
14
|
+
def initialize(id, page, parent_id = nil)
|
15
|
+
@id, @page, @parent_id = id, page, parent_id
|
16
|
+
end
|
17
|
+
|
18
|
+
# Can be one of:
|
19
|
+
# * started_loading
|
20
|
+
# * navigated
|
21
|
+
# * scheduled_navigation
|
22
|
+
# * cleared_scheduled_navigation
|
23
|
+
# * stopped_loading
|
24
|
+
def state=(value)
|
25
|
+
@state = value
|
26
|
+
end
|
27
|
+
|
28
|
+
def url
|
29
|
+
evaluate("document.location.href")
|
30
|
+
end
|
31
|
+
|
32
|
+
def title
|
33
|
+
evaluate("document.title")
|
34
|
+
end
|
35
|
+
|
36
|
+
def main?
|
37
|
+
@parent_id.nil?
|
38
|
+
end
|
39
|
+
|
40
|
+
def execution_id
|
41
|
+
raise NoExecutionContextError unless @execution_id
|
42
|
+
@execution_id
|
43
|
+
rescue NoExecutionContextError
|
44
|
+
@page.event.reset
|
45
|
+
@page.event.wait(@page.timeout) ? retry : raise
|
46
|
+
end
|
47
|
+
|
48
|
+
def inspect
|
49
|
+
%(#<#{self.class} @id=#{@id.inspect} @parent_id=#{@parent_id.inspect} @name=#{@name.inspect} @state=#{@state.inspect} @execution_id=#{@execution_id.inspect}>)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -18,16 +18,20 @@
|
|
18
18
|
# inspected node (DOM.pushNodesByBackendIdsToFrontend) or described in more
|
19
19
|
# details (DOM.describeNode).
|
20
20
|
module Ferrum
|
21
|
-
class
|
21
|
+
class Frame
|
22
22
|
module DOM
|
23
23
|
def current_url
|
24
24
|
evaluate("window.top.location.href")
|
25
25
|
end
|
26
26
|
|
27
|
-
def
|
27
|
+
def current_title
|
28
28
|
evaluate("window.top.document.title")
|
29
29
|
end
|
30
30
|
|
31
|
+
def doctype
|
32
|
+
evaluate("new XMLSerializer().serializeToString(document.doctype)")
|
33
|
+
end
|
34
|
+
|
31
35
|
def body
|
32
36
|
evaluate("document.documentElement.outerHTML")
|
33
37
|
end
|
@@ -57,22 +61,23 @@ module Ferrum
|
|
57
61
|
} else {
|
58
62
|
throw error;
|
59
63
|
}
|
60
|
-
}), timeout, selector, within)
|
64
|
+
}), @page.timeout, selector, within)
|
61
65
|
end
|
62
66
|
|
67
|
+
# FIXME css doesn't work for a frame w/o execution_id
|
63
68
|
def css(selector, within: nil)
|
64
|
-
node_id = within&.node_id || @document_id
|
69
|
+
node_id = within&.node_id || @page.document_id
|
65
70
|
|
66
|
-
ids = command("DOM.querySelectorAll",
|
67
|
-
|
68
|
-
|
71
|
+
ids = @page.command("DOM.querySelectorAll",
|
72
|
+
nodeId: node_id,
|
73
|
+
selector: selector)["nodeIds"]
|
69
74
|
ids.map { |id| build_node(id) }.compact
|
70
75
|
end
|
71
76
|
|
72
77
|
def at_css(selector, within: nil)
|
73
|
-
node_id = within&.node_id || @document_id
|
78
|
+
node_id = within&.node_id || @page.document_id
|
74
79
|
|
75
|
-
id = command("DOM.querySelector",
|
80
|
+
id = @page.command("DOM.querySelector",
|
76
81
|
nodeId: node_id,
|
77
82
|
selector: selector)["nodeId"]
|
78
83
|
build_node(id)
|
@@ -81,8 +86,8 @@ module Ferrum
|
|
81
86
|
private
|
82
87
|
|
83
88
|
def build_node(node_id)
|
84
|
-
description = command("DOM.describeNode", nodeId: node_id)
|
85
|
-
Node.new(self, target_id, node_id, description["node"])
|
89
|
+
description = @page.command("DOM.describeNode", nodeId: node_id)
|
90
|
+
Node.new(self, @page.target_id, node_id, description["node"])
|
86
91
|
end
|
87
92
|
end
|
88
93
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Ferrum
|
4
|
-
class
|
4
|
+
class Frame
|
5
5
|
module Runtime
|
6
6
|
INTERMITTENT_ATTEMPTS = ENV.fetch("FERRUM_INTERMITTENT_ATTEMPTS", 6).to_i
|
7
7
|
INTERMITTENT_SLEEP = ENV.fetch("FERRUM_INTERMITTENT_SLEEP", 0.1).to_f
|
@@ -31,6 +31,35 @@ module Ferrum
|
|
31
31
|
)
|
32
32
|
}.freeze
|
33
33
|
|
34
|
+
SCRIPT_SRC_TAG = <<~JS
|
35
|
+
const script = document.createElement("script");
|
36
|
+
script.src = arguments[0];
|
37
|
+
script.type = arguments[1];
|
38
|
+
script.onload = arguments[2];
|
39
|
+
document.head.appendChild(script);
|
40
|
+
JS
|
41
|
+
SCRIPT_TEXT_TAG = <<~JS
|
42
|
+
const script = document.createElement("script");
|
43
|
+
script.text = arguments[0];
|
44
|
+
script.type = arguments[1];
|
45
|
+
document.head.appendChild(script);
|
46
|
+
arguments[2]();
|
47
|
+
JS
|
48
|
+
STYLE_TAG = <<~JS
|
49
|
+
const style = document.createElement("style");
|
50
|
+
style.type = "text/css";
|
51
|
+
style.appendChild(document.createTextNode(arguments[0]));
|
52
|
+
document.head.appendChild(style);
|
53
|
+
arguments[1]();
|
54
|
+
JS
|
55
|
+
LINK_TAG = <<~JS
|
56
|
+
const link = document.createElement("link");
|
57
|
+
link.rel = "stylesheet";
|
58
|
+
link.href = arguments[0];
|
59
|
+
link.onload = arguments[1];
|
60
|
+
document.head.appendChild(link);
|
61
|
+
JS
|
62
|
+
|
34
63
|
def evaluate(expression, *args)
|
35
64
|
call(*args, expression: expression)
|
36
65
|
end
|
@@ -49,13 +78,13 @@ module Ferrum
|
|
49
78
|
attempts, sleep = INTERMITTENT_ATTEMPTS, INTERMITTENT_SLEEP
|
50
79
|
|
51
80
|
Ferrum.with_attempts(errors: errors, max: attempts, wait: sleep) do
|
52
|
-
response = command("DOM.resolveNode", nodeId: node.node_id)
|
81
|
+
response = @page.command("DOM.resolveNode", nodeId: node.node_id)
|
53
82
|
object_id = response.dig("object", "objectId")
|
54
83
|
options = DEFAULT_OPTIONS.merge(objectId: object_id)
|
55
84
|
options[:functionDeclaration] = options[:functionDeclaration] % expression
|
56
85
|
options.merge!(returnByValue: by_value)
|
57
86
|
|
58
|
-
response = command("Runtime.callFunctionOn",
|
87
|
+
response = @page.command("Runtime.callFunctionOn",
|
59
88
|
wait: wait, **options)["result"]
|
60
89
|
.tap { |r| handle_error(r) }
|
61
90
|
|
@@ -63,6 +92,34 @@ module Ferrum
|
|
63
92
|
end
|
64
93
|
end
|
65
94
|
|
95
|
+
def add_script_tag(url: nil, path: nil, content: nil, type: "text/javascript")
|
96
|
+
expr, *args = if url
|
97
|
+
[SCRIPT_SRC_TAG, url, type]
|
98
|
+
elsif path || content
|
99
|
+
if path
|
100
|
+
content = File.read(path)
|
101
|
+
content += "\n//# sourceURL=#{path}"
|
102
|
+
end
|
103
|
+
[SCRIPT_TEXT_TAG, content, type]
|
104
|
+
end
|
105
|
+
|
106
|
+
evaluate_async(expr, @page.timeout, *args)
|
107
|
+
end
|
108
|
+
|
109
|
+
def add_style_tag(url: nil, path: nil, content: nil)
|
110
|
+
expr, *args = if url
|
111
|
+
[LINK_TAG, url]
|
112
|
+
elsif path || content
|
113
|
+
if path
|
114
|
+
content = File.read(path)
|
115
|
+
content += "\n//# sourceURL=#{path}"
|
116
|
+
end
|
117
|
+
[STYLE_TAG, content]
|
118
|
+
end
|
119
|
+
|
120
|
+
evaluate_async(expr, @page.timeout, *args)
|
121
|
+
end
|
122
|
+
|
66
123
|
private
|
67
124
|
|
68
125
|
def call(*args, expression:, wait_time: nil, handle: true, **options)
|
@@ -76,10 +133,10 @@ module Ferrum
|
|
76
133
|
params[:functionDeclaration] = params[:functionDeclaration] % expression
|
77
134
|
params = params.merge(arguments: arguments)
|
78
135
|
unless params[:executionContextId]
|
79
|
-
params = params.merge(executionContextId:
|
136
|
+
params = params.merge(executionContextId: execution_id)
|
80
137
|
end
|
81
138
|
|
82
|
-
response = command("Runtime.callFunctionOn",
|
139
|
+
response = @page.command("Runtime.callFunctionOn",
|
83
140
|
**params)["result"].tap { |r| handle_error(r) }
|
84
141
|
|
85
142
|
handle ? handle_response(response) : response
|
@@ -115,9 +172,9 @@ module Ferrum
|
|
115
172
|
# and node destroyed so we need to retrieve it each time for given id.
|
116
173
|
# Though we can try to subscribe to `DOM.childNodeRemoved` and
|
117
174
|
# `DOM.childNodeInserted` in the future.
|
118
|
-
node_id = command("DOM.requestNode", objectId: object_id)["nodeId"]
|
119
|
-
description = command("DOM.describeNode", nodeId: node_id)["node"]
|
120
|
-
Node.new(self, target_id, node_id, description)
|
175
|
+
node_id = @page.command("DOM.requestNode", objectId: object_id)["nodeId"]
|
176
|
+
description = @page.command("DOM.describeNode", nodeId: node_id)["node"]
|
177
|
+
Node.new(self, @page.target_id, node_id, description)
|
121
178
|
when "array"
|
122
179
|
reduce_props(object_id, []) do |memo, key, value|
|
123
180
|
next(memo) unless (Integer(key) rescue nil)
|
@@ -140,7 +197,7 @@ module Ferrum
|
|
140
197
|
def prepare_args(args)
|
141
198
|
args.map do |arg|
|
142
199
|
if arg.is_a?(Node)
|
143
|
-
resolved = command("DOM.resolveNode", nodeId: arg.node_id)
|
200
|
+
resolved = @page.command("DOM.resolveNode", nodeId: arg.node_id)
|
144
201
|
{ objectId: resolved["object"]["objectId"] }
|
145
202
|
elsif arg.is_a?(Hash) && arg["objectId"]
|
146
203
|
{ objectId: arg["objectId"] }
|
@@ -154,7 +211,7 @@ module Ferrum
|
|
154
211
|
if cyclic?(object_id).dig("result", "value")
|
155
212
|
return "(cyclic structure)"
|
156
213
|
else
|
157
|
-
props = command("Runtime.getProperties", objectId: object_id)
|
214
|
+
props = @page.command("Runtime.getProperties", ownProperties: true, objectId: object_id)
|
158
215
|
props["result"].reduce(to) do |memo, prop|
|
159
216
|
next(memo) unless prop["enumerable"]
|
160
217
|
yield(memo, prop["name"], prop["value"])
|
@@ -163,7 +220,7 @@ module Ferrum
|
|
163
220
|
end
|
164
221
|
|
165
222
|
def cyclic?(object_id)
|
166
|
-
command("Runtime.callFunctionOn",
|
223
|
+
@page.command("Runtime.callFunctionOn",
|
167
224
|
objectId: object_id,
|
168
225
|
returnByValue: true,
|
169
226
|
functionDeclaration: <<~JS
|
data/lib/ferrum/network.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "ferrum/network/exchange"
|
4
4
|
require "ferrum/network/intercepted_request"
|
5
|
+
require "ferrum/network/auth_request"
|
5
6
|
|
6
7
|
module Ferrum
|
7
8
|
class Network
|
@@ -51,7 +52,7 @@ module Ferrum
|
|
51
52
|
pattern[:resourceType] = resource_type
|
52
53
|
end
|
53
54
|
|
54
|
-
@page.command("
|
55
|
+
@page.command("Fetch.enable", handleAuthRequests: true, patterns: [pattern])
|
55
56
|
end
|
56
57
|
|
57
58
|
def authorize(user:, password:, type: :server)
|
@@ -64,18 +65,22 @@ module Ferrum
|
|
64
65
|
|
65
66
|
intercept
|
66
67
|
|
67
|
-
@page.on(:request) do |request
|
68
|
+
@page.on(:request) do |request|
|
69
|
+
request.continue
|
70
|
+
end
|
71
|
+
|
72
|
+
@page.on(:auth) do |request, index, total|
|
68
73
|
if request.auth_challenge?(type)
|
69
74
|
response = authorized_response(@authorized_ids[type],
|
70
|
-
request.
|
75
|
+
request.request_id,
|
71
76
|
user, password)
|
72
77
|
|
73
|
-
@authorized_ids[type] << request.
|
78
|
+
@authorized_ids[type] << request.request_id
|
74
79
|
request.continue(authChallengeResponse: response)
|
75
80
|
elsif index + 1 < total
|
76
81
|
next # There are other callbacks that can handle this
|
77
82
|
else
|
78
|
-
request.
|
83
|
+
request.abort
|
79
84
|
end
|
80
85
|
end
|
81
86
|
end
|
@@ -90,8 +95,8 @@ module Ferrum
|
|
90
95
|
exchange.build_response(params)
|
91
96
|
end
|
92
97
|
|
93
|
-
exchange = Network::Exchange.new(params)
|
94
|
-
@exchange = exchange if exchange.navigation_request?(@page.
|
98
|
+
exchange = Network::Exchange.new(@page, params)
|
99
|
+
@exchange = exchange if exchange.navigation_request?(@page.main_frame.id)
|
95
100
|
@traffic << exchange
|
96
101
|
end
|
97
102
|
|
@@ -118,8 +123,8 @@ module Ferrum
|
|
118
123
|
end
|
119
124
|
end
|
120
125
|
|
121
|
-
def authorized_response(ids,
|
122
|
-
if ids.include?(
|
126
|
+
def authorized_response(ids, request_id, username, password)
|
127
|
+
if ids.include?(request_id)
|
123
128
|
{ response: "CancelAuth" }
|
124
129
|
elsif username && password
|
125
130
|
{ response: "ProvideCredentials",
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ferrum
|
4
|
+
class Network
|
5
|
+
class AuthRequest
|
6
|
+
attr_accessor :request_id, :frame_id, :resource_type
|
7
|
+
|
8
|
+
def initialize(page, params)
|
9
|
+
@page, @params = page, params
|
10
|
+
@request_id = params["requestId"]
|
11
|
+
@frame_id = params["frameId"]
|
12
|
+
@resource_type = params["resourceType"]
|
13
|
+
@request = params["request"]
|
14
|
+
end
|
15
|
+
|
16
|
+
def navigation_request?
|
17
|
+
@params["isNavigationRequest"]
|
18
|
+
end
|
19
|
+
|
20
|
+
def auth_challenge?(source)
|
21
|
+
@params.dig("authChallenge", "source")&.downcase&.to_s == source.to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
def match?(regexp)
|
25
|
+
!!url.match(regexp)
|
26
|
+
end
|
27
|
+
|
28
|
+
def continue(**options)
|
29
|
+
options = options.merge(requestId: request_id)
|
30
|
+
@page.command("Fetch.continueWithAuth", **options)
|
31
|
+
end
|
32
|
+
|
33
|
+
def abort
|
34
|
+
@page.command("Fetch.failRequest", requestId: request_id, errorReason: "BlockedByClient")
|
35
|
+
end
|
36
|
+
|
37
|
+
def url
|
38
|
+
@request["url"]
|
39
|
+
end
|
40
|
+
|
41
|
+
def method
|
42
|
+
@request["method"]
|
43
|
+
end
|
44
|
+
|
45
|
+
def headers
|
46
|
+
@request["headers"]
|
47
|
+
end
|
48
|
+
|
49
|
+
def initial_priority
|
50
|
+
@request["initialPriority"]
|
51
|
+
end
|
52
|
+
|
53
|
+
def referrer_policy
|
54
|
+
@request["referrerPolicy"]
|
55
|
+
end
|
56
|
+
|
57
|
+
def inspect
|
58
|
+
%(#<#{self.class} @request_id=#{@request_id.inspect} @frame_id=#{@frame_id.inspect} @resource_type=#{@resource_type.inspect} @request=#{@request.inspect}>)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -9,7 +9,8 @@ module Ferrum
|
|
9
9
|
class Exchange
|
10
10
|
attr_reader :request, :response, :error
|
11
11
|
|
12
|
-
def initialize(params)
|
12
|
+
def initialize(page, params)
|
13
|
+
@page = page
|
13
14
|
@response = @error = nil
|
14
15
|
build_request(params)
|
15
16
|
end
|
@@ -19,7 +20,7 @@ module Ferrum
|
|
19
20
|
end
|
20
21
|
|
21
22
|
def build_response(params)
|
22
|
-
@response = Network::Response.new(params)
|
23
|
+
@response = Network::Response.new(@page, params)
|
23
24
|
end
|
24
25
|
|
25
26
|
def build_error(params)
|
@@ -38,6 +39,10 @@ module Ferrum
|
|
38
39
|
def to_a
|
39
40
|
[request, response, error]
|
40
41
|
end
|
42
|
+
|
43
|
+
def inspect
|
44
|
+
%(#<#{self.class} @id=#{@id.inspect} @request=#{@request.inspect} @response=#{@response.inspect} @error=#{@error.inspect}>)
|
45
|
+
end
|
41
46
|
end
|
42
47
|
end
|
43
48
|
end
|
@@ -1,13 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "base64"
|
4
|
+
|
3
5
|
module Ferrum
|
4
6
|
class Network
|
5
7
|
class InterceptedRequest
|
6
|
-
attr_accessor :
|
8
|
+
attr_accessor :request_id, :frame_id, :resource_type
|
7
9
|
|
8
10
|
def initialize(page, params)
|
9
11
|
@page, @params = page, params
|
10
|
-
@
|
12
|
+
@request_id = params["requestId"]
|
11
13
|
@frame_id = params["frameId"]
|
12
14
|
@resource_type = params["resourceType"]
|
13
15
|
@request = params["request"]
|
@@ -17,21 +19,32 @@ module Ferrum
|
|
17
19
|
@params["isNavigationRequest"]
|
18
20
|
end
|
19
21
|
|
20
|
-
def auth_challenge?(source)
|
21
|
-
@params.dig("authChallenge", "source")&.downcase&.to_s == source.to_s
|
22
|
-
end
|
23
|
-
|
24
22
|
def match?(regexp)
|
25
23
|
!!url.match(regexp)
|
26
24
|
end
|
27
25
|
|
26
|
+
def respond(**options)
|
27
|
+
has_body = options.has_key?(:body)
|
28
|
+
headers = has_body ? { "content-length" => options.fetch(:body, '').length } : {}
|
29
|
+
headers = headers.merge(options.fetch(:responseHeaders, {}))
|
30
|
+
|
31
|
+
options = {responseCode: 200}.merge(options)
|
32
|
+
options = options.merge({
|
33
|
+
requestId: request_id,
|
34
|
+
responseHeaders: header_array(headers),
|
35
|
+
})
|
36
|
+
options = options.merge(body: Base64.encode64(options.fetch(:body, '')).strip) if has_body
|
37
|
+
|
38
|
+
@page.command("Fetch.fulfillRequest", **options)
|
39
|
+
end
|
40
|
+
|
28
41
|
def continue(**options)
|
29
|
-
options = options.merge(
|
30
|
-
@page.command("
|
42
|
+
options = options.merge(requestId: request_id)
|
43
|
+
@page.command("Fetch.continueRequest", **options)
|
31
44
|
end
|
32
45
|
|
33
46
|
def abort
|
34
|
-
|
47
|
+
@page.command("Fetch.failRequest", requestId: request_id, errorReason: "BlockedByClient")
|
35
48
|
end
|
36
49
|
|
37
50
|
def url
|
@@ -55,7 +68,15 @@ module Ferrum
|
|
55
68
|
end
|
56
69
|
|
57
70
|
def inspect
|
58
|
-
%(#<#{self.class} @
|
71
|
+
%(#<#{self.class} @request_id=#{@request_id.inspect} @frame_id=#{@frame_id.inspect} @resource_type=#{@resource_type.inspect} @request=#{@request.inspect}>)
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def header_array(values)
|
77
|
+
values.map do |key, value|
|
78
|
+
{ name: String(key), value: String(value) }
|
79
|
+
end
|
59
80
|
end
|
60
81
|
end
|
61
82
|
end
|
@@ -5,9 +5,10 @@ module Ferrum
|
|
5
5
|
class Response
|
6
6
|
attr_reader :body_size
|
7
7
|
|
8
|
-
def initialize(params)
|
8
|
+
def initialize(page, params)
|
9
|
+
@page = page
|
9
10
|
@params = params
|
10
|
-
@response = params["response"] ||
|
11
|
+
@response = params["response"] || params["redirectResponse"]
|
11
12
|
end
|
12
13
|
|
13
14
|
def id
|
@@ -35,7 +36,7 @@ module Ferrum
|
|
35
36
|
end
|
36
37
|
|
37
38
|
def content_type
|
38
|
-
@content_type ||=
|
39
|
+
@content_type ||= headers.find { |k, _| k.downcase == "content-type" }&.last&.sub(/;.*\z/, "")
|
39
40
|
end
|
40
41
|
|
41
42
|
# See https://crbug.com/883475
|
@@ -46,6 +47,27 @@ module Ferrum
|
|
46
47
|
def body_size=(size)
|
47
48
|
@body_size = size - headers_size
|
48
49
|
end
|
50
|
+
|
51
|
+
def body
|
52
|
+
@body ||= begin
|
53
|
+
body, encoded = @page
|
54
|
+
.command("Network.getResponseBody", requestId: id)
|
55
|
+
.values_at("body", "base64Encoded")
|
56
|
+
encoded ? Base64.decode64(body) : body
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def main?
|
61
|
+
@page.network.response == self
|
62
|
+
end
|
63
|
+
|
64
|
+
def ==(other)
|
65
|
+
id == other.id
|
66
|
+
end
|
67
|
+
|
68
|
+
def inspect
|
69
|
+
%(#<#{self.class} @params=#{@params.inspect} @response=#{@response.inspect}>)
|
70
|
+
end
|
49
71
|
end
|
50
72
|
end
|
51
73
|
end
|
data/lib/ferrum/node.rb
CHANGED
@@ -4,8 +4,9 @@ module Ferrum
|
|
4
4
|
class Node
|
5
5
|
attr_reader :page, :target_id, :node_id, :description, :tag_name
|
6
6
|
|
7
|
-
def initialize(
|
8
|
-
@page
|
7
|
+
def initialize(frame, target_id, node_id, description)
|
8
|
+
@page = frame.page
|
9
|
+
@target_id = target_id
|
9
10
|
@node_id, @description = node_id, description
|
10
11
|
@tag_name = description["nodeName"].downcase
|
11
12
|
end
|
@@ -14,6 +15,14 @@ module Ferrum
|
|
14
15
|
description["nodeType"] == 1 # nodeType: 3, nodeName: "#text" e.g.
|
15
16
|
end
|
16
17
|
|
18
|
+
def frame_id
|
19
|
+
description["frameId"]
|
20
|
+
end
|
21
|
+
|
22
|
+
def frame
|
23
|
+
page.frame_by(id: frame_id)
|
24
|
+
end
|
25
|
+
|
17
26
|
def focus
|
18
27
|
tap { page.command("DOM.focus", nodeId: node_id) }
|
19
28
|
end
|
data/lib/ferrum/page.rb
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "forwardable"
|
3
4
|
require "ferrum/mouse"
|
4
5
|
require "ferrum/keyboard"
|
5
6
|
require "ferrum/headers"
|
6
7
|
require "ferrum/cookies"
|
7
8
|
require "ferrum/dialog"
|
8
9
|
require "ferrum/network"
|
9
|
-
require "ferrum/page/
|
10
|
-
require "ferrum/page/runtime"
|
11
|
-
require "ferrum/page/frame"
|
10
|
+
require "ferrum/page/frames"
|
12
11
|
require "ferrum/page/screenshot"
|
13
12
|
require "ferrum/browser/client"
|
14
13
|
|
@@ -28,21 +27,24 @@ module Ferrum
|
|
28
27
|
end
|
29
28
|
end
|
30
29
|
|
31
|
-
|
30
|
+
extend Forwardable
|
31
|
+
delegate %i[at_css at_xpath css xpath
|
32
|
+
current_url current_title url title body doctype
|
33
|
+
execution_id evaluate evaluate_on evaluate_async execute
|
34
|
+
add_script_tag add_style_tag] => :main_frame
|
35
|
+
|
36
|
+
include Frames, Screenshot
|
32
37
|
|
33
38
|
attr_accessor :referrer
|
34
39
|
attr_reader :target_id, :browser,
|
35
40
|
:headers, :cookies, :network,
|
36
|
-
:mouse, :keyboard
|
41
|
+
:mouse, :keyboard, :event, :document_id
|
37
42
|
|
38
43
|
def initialize(target_id, browser)
|
44
|
+
@frames = {}
|
39
45
|
@target_id, @browser = target_id, browser
|
40
46
|
@event = Event.new.tap(&:set)
|
41
47
|
|
42
|
-
@frames = {}
|
43
|
-
@waiting_frames ||= Set.new
|
44
|
-
@frame_stack = []
|
45
|
-
|
46
48
|
host = @browser.process.host
|
47
49
|
port = @browser.process.port
|
48
50
|
ws_url = "ws://#{host}:#{port}/devtools/page/#{@target_id}"
|
@@ -85,12 +87,18 @@ module Ferrum
|
|
85
87
|
@window_id, @bounds = result.values_at("windowId", "bounds")
|
86
88
|
|
87
89
|
if fullscreen
|
90
|
+
width, height = document_size
|
88
91
|
@browser.command("Browser.setWindowBounds", windowId: @window_id, bounds: { windowState: "fullscreen" })
|
89
92
|
else
|
90
93
|
@browser.command("Browser.setWindowBounds", windowId: @window_id, bounds: { windowState: "normal" })
|
91
94
|
@browser.command("Browser.setWindowBounds", windowId: @window_id, bounds: { width: width, height: height, windowState: "normal" })
|
92
|
-
command("Emulation.setDeviceMetricsOverride", width: width, height: height, deviceScaleFactor: 1, mobile: false)
|
93
95
|
end
|
96
|
+
|
97
|
+
command("Emulation.setDeviceMetricsOverride", width: width,
|
98
|
+
height: height,
|
99
|
+
deviceScaleFactor: 1,
|
100
|
+
mobile: false,
|
101
|
+
fitWindow: false)
|
94
102
|
end
|
95
103
|
|
96
104
|
def refresh
|
@@ -123,10 +131,15 @@ module Ferrum
|
|
123
131
|
block.call(dialog, index, total)
|
124
132
|
end
|
125
133
|
when :request
|
126
|
-
@client.on("
|
134
|
+
@client.on("Fetch.requestPaused") do |params, index, total|
|
127
135
|
request = Network::InterceptedRequest.new(self, params)
|
128
136
|
block.call(request, index, total)
|
129
137
|
end
|
138
|
+
when :auth
|
139
|
+
@client.on("Fetch.authRequired") do |params, index, total|
|
140
|
+
request = Network::AuthRequest.new(self, params)
|
141
|
+
block.call(request, index, total)
|
142
|
+
end
|
130
143
|
else
|
131
144
|
@client.on(name, &block)
|
132
145
|
end
|
@@ -135,19 +148,9 @@ module Ferrum
|
|
135
148
|
private
|
136
149
|
|
137
150
|
def subscribe
|
138
|
-
|
139
|
-
|
151
|
+
frames_subscribe
|
140
152
|
network.subscribe
|
141
153
|
|
142
|
-
on("Network.loadingFailed") do |params|
|
143
|
-
id, canceled = params.values_at("requestId", "canceled")
|
144
|
-
# Set event as we aborted main request we are waiting for
|
145
|
-
if network.request&.id == id && canceled == true
|
146
|
-
@event.set
|
147
|
-
@document_id = get_document_id
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
154
|
if @browser.logger
|
152
155
|
on("Runtime.consoleAPICalled") do |params|
|
153
156
|
params["args"].each { |r| @browser.logger.puts(r["value"]) }
|
@@ -161,24 +164,20 @@ module Ferrum
|
|
161
164
|
end
|
162
165
|
end
|
163
166
|
|
164
|
-
on("Page.navigatedWithinDocument") do
|
165
|
-
@event.set if @waiting_frames.empty?
|
166
|
-
end
|
167
|
-
|
168
167
|
on("Page.domContentEventFired") do |params|
|
169
168
|
# `frameStoppedLoading` doesn't occur if status isn't success
|
170
169
|
if network.status != 200
|
171
170
|
@event.set
|
172
|
-
|
171
|
+
get_document_id
|
173
172
|
end
|
174
173
|
end
|
175
174
|
end
|
176
175
|
|
177
176
|
def prepare_page
|
178
177
|
command("Page.enable")
|
178
|
+
command("Runtime.enable")
|
179
179
|
command("DOM.enable")
|
180
180
|
command("CSS.enable")
|
181
|
-
command("Runtime.enable")
|
182
181
|
command("Log.enable")
|
183
182
|
command("Network.enable")
|
184
183
|
|
@@ -202,7 +201,7 @@ module Ferrum
|
|
202
201
|
# occurs and thus search for nodes cannot be completed. Here we check
|
203
202
|
# the history and if the transitionType for example `link` then
|
204
203
|
# content is already loaded and we can try to get the document.
|
205
|
-
|
204
|
+
get_document_id
|
206
205
|
end
|
207
206
|
end
|
208
207
|
|
@@ -214,7 +213,7 @@ module Ferrum
|
|
214
213
|
# We also evaluate script just in case because
|
215
214
|
# `Page.addScriptToEvaluateOnNewDocument` doesn't work in popups.
|
216
215
|
command("Runtime.evaluate", expression: extension,
|
217
|
-
contextId:
|
216
|
+
contextId: execution_id,
|
218
217
|
returnByValue: true)
|
219
218
|
end
|
220
219
|
end
|
@@ -241,7 +240,7 @@ module Ferrum
|
|
241
240
|
end
|
242
241
|
|
243
242
|
def get_document_id
|
244
|
-
command("DOM.getDocument", depth: 0).dig("root", "nodeId")
|
243
|
+
@document_id = command("DOM.getDocument", depth: 0).dig("root", "nodeId")
|
245
244
|
end
|
246
245
|
end
|
247
246
|
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ferrum/frame"
|
4
|
+
|
5
|
+
module Ferrum
|
6
|
+
class Page
|
7
|
+
module Frames
|
8
|
+
attr_reader :main_frame
|
9
|
+
|
10
|
+
def frames
|
11
|
+
@frames.values
|
12
|
+
end
|
13
|
+
|
14
|
+
def frame_by(id: nil, execution_id: nil, name: nil)
|
15
|
+
if id
|
16
|
+
@frames[id]
|
17
|
+
elsif execution_id
|
18
|
+
frames.find { |f| f.execution_id == execution_id }
|
19
|
+
elsif name
|
20
|
+
frames.find { |f| f.name == name }
|
21
|
+
else
|
22
|
+
raise ArgumentError
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def frames_subscribe
|
27
|
+
on("Page.frameAttached") do |params|
|
28
|
+
parent_frame_id, frame_id = params.values_at("parentFrameId", "frameId")
|
29
|
+
@frames[frame_id] = Frame.new(frame_id, self, parent_frame_id)
|
30
|
+
end
|
31
|
+
|
32
|
+
on("Page.frameStartedLoading") do |params|
|
33
|
+
frame = @frames[params["frameId"]]
|
34
|
+
frame.state = :started_loading
|
35
|
+
@event.reset
|
36
|
+
end
|
37
|
+
|
38
|
+
on("Page.frameNavigated") do |params|
|
39
|
+
frame_id, name = params["frame"]&.values_at("id", "name")
|
40
|
+
frame = @frames[frame_id]
|
41
|
+
frame.state = :navigated
|
42
|
+
frame.name = name unless name.to_s.empty?
|
43
|
+
end
|
44
|
+
|
45
|
+
on("Page.frameScheduledNavigation") do |params|
|
46
|
+
frame = @frames[params["frameId"]]
|
47
|
+
frame.state = :scheduled_navigation
|
48
|
+
@event.reset
|
49
|
+
end
|
50
|
+
|
51
|
+
on("Page.frameClearedScheduledNavigation") do |params|
|
52
|
+
frame = @frames[params["frameId"]]
|
53
|
+
frame.state = :cleared_scheduled_navigation
|
54
|
+
@event.set if idling?
|
55
|
+
end
|
56
|
+
|
57
|
+
on("Page.frameStoppedLoading") do |params|
|
58
|
+
# `DOM.performSearch` doesn't work without getting #document node first.
|
59
|
+
# It returns node with nodeId 1 and nodeType 9 from which descend the
|
60
|
+
# tree and we save it in a variable because if we call that again root
|
61
|
+
# node will change the id and all subsequent nodes have to change id too.
|
62
|
+
if @main_frame.id == params["frameId"]
|
63
|
+
@event.set if idling?
|
64
|
+
get_document_id
|
65
|
+
end
|
66
|
+
|
67
|
+
frame = @frames[params["frameId"]]
|
68
|
+
frame.state = :stopped_loading
|
69
|
+
|
70
|
+
@event.set if idling?
|
71
|
+
end
|
72
|
+
|
73
|
+
on("Page.navigatedWithinDocument") do
|
74
|
+
@event.set if idling?
|
75
|
+
end
|
76
|
+
|
77
|
+
on("Network.requestWillBeSent") do |params|
|
78
|
+
if params["frameId"] == @main_frame.id
|
79
|
+
# Possible types:
|
80
|
+
# Document, Stylesheet, Image, Media, Font, Script, TextTrack, XHR,
|
81
|
+
# Fetch, EventSource, WebSocket, Manifest, SignedExchange, Ping,
|
82
|
+
# CSPViolationReport, Other
|
83
|
+
@event.reset if params["type"] == "Document"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
on("Runtime.executionContextCreated") do |params|
|
88
|
+
context_id = params.dig("context", "id")
|
89
|
+
frame_id = params.dig("context", "auxData", "frameId")
|
90
|
+
frame = @frames[frame_id] || Frame.new(frame_id, self)
|
91
|
+
frame.execution_id = context_id
|
92
|
+
|
93
|
+
@main_frame ||= frame
|
94
|
+
@frames[frame_id] ||= frame
|
95
|
+
end
|
96
|
+
|
97
|
+
on("Runtime.executionContextDestroyed") do |params|
|
98
|
+
execution_id = params["executionContextId"]
|
99
|
+
frame = frame_by(execution_id: execution_id)
|
100
|
+
frame.execution_id = nil
|
101
|
+
end
|
102
|
+
|
103
|
+
on("Runtime.executionContextsCleared") do
|
104
|
+
@frames.delete_if { |_, f| !f.main? }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def idling?
|
111
|
+
@frames.all? { |_, f| f.state == :stopped_loading }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -3,10 +3,31 @@
|
|
3
3
|
module Ferrum
|
4
4
|
class Page
|
5
5
|
module Screenshot
|
6
|
+
DEFAULT_PDF_OPTIONS = {
|
7
|
+
landscape: false,
|
8
|
+
paper_width: 8.5,
|
9
|
+
paper_height: 11,
|
10
|
+
scale: 1.0
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
PAPEP_FORMATS = {
|
14
|
+
letter: { width: 8.50, height: 11.00 },
|
15
|
+
legal: { width: 8.50, height: 14.00 },
|
16
|
+
tabloid: { width: 11.00, height: 17.00 },
|
17
|
+
ledger: { width: 17.00, height: 11.00 },
|
18
|
+
A0: { width: 33.10, height: 46.80 },
|
19
|
+
A1: { width: 23.40, height: 33.10 },
|
20
|
+
A2: { width: 16.54, height: 23.40 },
|
21
|
+
A3: { width: 11.70, height: 16.54 },
|
22
|
+
A4: { width: 8.27, height: 11.70 },
|
23
|
+
A5: { width: 5.83, height: 8.27 },
|
24
|
+
A6: { width: 4.13, height: 5.83 },
|
25
|
+
}.freeze
|
26
|
+
|
6
27
|
def screenshot(**opts)
|
7
28
|
path, encoding = common_options(**opts)
|
8
29
|
options = screenshot_options(path, **opts)
|
9
|
-
data =
|
30
|
+
data = capture_screenshot(options, opts[:full])
|
10
31
|
return data if encoding == :base64
|
11
32
|
save_file(path, data)
|
12
33
|
end
|
@@ -46,13 +67,21 @@ module Ferrum
|
|
46
67
|
[path, encoding]
|
47
68
|
end
|
48
69
|
|
49
|
-
def pdf_options(
|
50
|
-
|
51
|
-
options
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
70
|
+
def pdf_options(**opts)
|
71
|
+
format = opts.delete(:format)
|
72
|
+
options = DEFAULT_PDF_OPTIONS.merge(opts)
|
73
|
+
|
74
|
+
if format
|
75
|
+
if opts[:paper_width] || opts[:paper_height]
|
76
|
+
raise ArgumentError, "Specify :format or :paper_width, :paper_height"
|
77
|
+
end
|
78
|
+
|
79
|
+
dimension = PAPEP_FORMATS.fetch(format)
|
80
|
+
options.merge!(paper_width: dimension[:width],
|
81
|
+
paper_height: dimension[:height])
|
82
|
+
end
|
83
|
+
|
84
|
+
options.map { |k, v| [to_camel_case(k), v] }.to_h
|
56
85
|
end
|
57
86
|
|
58
87
|
def screenshot_options(path = nil, format: nil, scale: 1.0, **opts)
|
@@ -73,8 +102,7 @@ module Ferrum
|
|
73
102
|
width, height = document_size
|
74
103
|
options.merge!(clip: { x: 0, y: 0, width: width, height: height, scale: scale }) if width > 0 && height > 0
|
75
104
|
elsif opts[:selector]
|
76
|
-
|
77
|
-
options.merge!(clip: { x: rect["x"], y: rect["y"], width: rect["width"], height: rect["height"], scale: scale })
|
105
|
+
options.merge!(clip: get_bounding_rect(opts[:selector]).merge(scale: scale))
|
78
106
|
end
|
79
107
|
|
80
108
|
if scale != 1.0
|
@@ -88,6 +116,40 @@ module Ferrum
|
|
88
116
|
|
89
117
|
options
|
90
118
|
end
|
119
|
+
|
120
|
+
def get_bounding_rect(selector)
|
121
|
+
rect = evaluate_async(%Q(
|
122
|
+
const rect = document
|
123
|
+
.querySelector('#{selector}')
|
124
|
+
.getBoundingClientRect();
|
125
|
+
const {x, y, width, height} = rect;
|
126
|
+
arguments[0]([x, y, width, height])
|
127
|
+
), timeout)
|
128
|
+
|
129
|
+
{ x: rect[0], y: rect[1], width: rect[2], height: rect[3] }
|
130
|
+
end
|
131
|
+
|
132
|
+
def to_camel_case(option)
|
133
|
+
return :preferCSSPageSize if option == :prefer_css_page_size
|
134
|
+
option.to_s.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.to_sym
|
135
|
+
end
|
136
|
+
|
137
|
+
def capture_screenshot(options, full)
|
138
|
+
maybe_resize_fullscreen(full) do
|
139
|
+
command("Page.captureScreenshot", options)
|
140
|
+
end.fetch("data")
|
141
|
+
end
|
142
|
+
|
143
|
+
def maybe_resize_fullscreen(full)
|
144
|
+
if full
|
145
|
+
width, height = viewport_size.dup
|
146
|
+
resize(fullscreen: true)
|
147
|
+
end
|
148
|
+
|
149
|
+
yield
|
150
|
+
ensure
|
151
|
+
resize(width: width, height: height) if full
|
152
|
+
end
|
91
153
|
end
|
92
154
|
end
|
93
155
|
end
|
data/lib/ferrum/target.rb
CHANGED
data/lib/ferrum/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ferrum
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.6'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dmitry Vorotilin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-10-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: websocket-driver
|
@@ -189,11 +189,15 @@ files:
|
|
189
189
|
- lib/ferrum/contexts.rb
|
190
190
|
- lib/ferrum/cookies.rb
|
191
191
|
- lib/ferrum/dialog.rb
|
192
|
+
- lib/ferrum/frame.rb
|
193
|
+
- lib/ferrum/frame/dom.rb
|
194
|
+
- lib/ferrum/frame/runtime.rb
|
192
195
|
- lib/ferrum/headers.rb
|
193
196
|
- lib/ferrum/keyboard.json
|
194
197
|
- lib/ferrum/keyboard.rb
|
195
198
|
- lib/ferrum/mouse.rb
|
196
199
|
- lib/ferrum/network.rb
|
200
|
+
- lib/ferrum/network/auth_request.rb
|
197
201
|
- lib/ferrum/network/error.rb
|
198
202
|
- lib/ferrum/network/exchange.rb
|
199
203
|
- lib/ferrum/network/intercepted_request.rb
|
@@ -201,9 +205,7 @@ files:
|
|
201
205
|
- lib/ferrum/network/response.rb
|
202
206
|
- lib/ferrum/node.rb
|
203
207
|
- lib/ferrum/page.rb
|
204
|
-
- lib/ferrum/page/
|
205
|
-
- lib/ferrum/page/frame.rb
|
206
|
-
- lib/ferrum/page/runtime.rb
|
208
|
+
- lib/ferrum/page/frames.rb
|
207
209
|
- lib/ferrum/page/screenshot.rb
|
208
210
|
- lib/ferrum/target.rb
|
209
211
|
- lib/ferrum/version.rb
|
data/lib/ferrum/page/frame.rb
DELETED
@@ -1,135 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Ferrum
|
4
|
-
class Page
|
5
|
-
module Frame
|
6
|
-
attr_reader :frame_id
|
7
|
-
|
8
|
-
def execution_context_id
|
9
|
-
context_id = current_execution_context_id
|
10
|
-
raise NoExecutionContextError unless context_id
|
11
|
-
context_id
|
12
|
-
rescue NoExecutionContextError
|
13
|
-
@event.reset
|
14
|
-
@event.wait(timeout) ? retry : raise
|
15
|
-
end
|
16
|
-
|
17
|
-
def frame_name
|
18
|
-
evaluate("window.name")
|
19
|
-
end
|
20
|
-
|
21
|
-
def frame_url
|
22
|
-
evaluate("window.location.href")
|
23
|
-
end
|
24
|
-
|
25
|
-
def frame_title
|
26
|
-
evaluate("document.title")
|
27
|
-
end
|
28
|
-
|
29
|
-
def within_frame(frame)
|
30
|
-
unless frame.is_a?(Node)
|
31
|
-
raise ArgumentError, "Node is expected, but #{frame.class} is given"
|
32
|
-
end
|
33
|
-
|
34
|
-
frame_id = frame.description["frameId"]
|
35
|
-
@frame_stack << frame_id
|
36
|
-
inject_extensions
|
37
|
-
yield
|
38
|
-
ensure
|
39
|
-
@frame_stack.pop
|
40
|
-
end
|
41
|
-
|
42
|
-
private
|
43
|
-
|
44
|
-
def subscribe
|
45
|
-
super if defined?(super)
|
46
|
-
|
47
|
-
on("Page.frameAttached") do |params|
|
48
|
-
@frames[params["frameId"]] = { "parent_id" => params["parentFrameId"] }
|
49
|
-
end
|
50
|
-
|
51
|
-
on("Page.frameStartedLoading") do |params|
|
52
|
-
@waiting_frames << params["frameId"]
|
53
|
-
@event.reset
|
54
|
-
end
|
55
|
-
|
56
|
-
on("Page.frameNavigated") do |params|
|
57
|
-
id = params["frame"]["id"]
|
58
|
-
if frame = @frames[id]
|
59
|
-
frame.merge!(params["frame"].select { |k, _| k == "name" || k == "url" })
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
on("Page.frameScheduledNavigation") do |params|
|
64
|
-
@waiting_frames << params["frameId"]
|
65
|
-
@event.reset
|
66
|
-
end
|
67
|
-
|
68
|
-
on("Page.frameStoppedLoading") do |params|
|
69
|
-
# `DOM.performSearch` doesn't work without getting #document node first.
|
70
|
-
# It returns node with nodeId 1 and nodeType 9 from which descend the
|
71
|
-
# tree and we save it in a variable because if we call that again root
|
72
|
-
# node will change the id and all subsequent nodes have to change id too.
|
73
|
-
if params["frameId"] == @frame_id
|
74
|
-
@event.set if @waiting_frames.empty?
|
75
|
-
@document_id = get_document_id
|
76
|
-
end
|
77
|
-
|
78
|
-
if @waiting_frames.include?(params["frameId"])
|
79
|
-
@waiting_frames.delete(params["frameId"])
|
80
|
-
@event.set if @waiting_frames.empty?
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
on("Network.requestWillBeSent") do |params|
|
85
|
-
if params["frameId"] == @frame_id
|
86
|
-
# Possible types:
|
87
|
-
# Document, Stylesheet, Image, Media, Font, Script, TextTrack, XHR,
|
88
|
-
# Fetch, EventSource, WebSocket, Manifest, SignedExchange, Ping,
|
89
|
-
# CSPViolationReport, Other
|
90
|
-
@event.reset if params["type"] == "Document"
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
on("Runtime.executionContextCreated") do |params|
|
95
|
-
context_id = params.dig("context", "id")
|
96
|
-
@execution_context_id ||= context_id
|
97
|
-
|
98
|
-
frame_id = params.dig("context", "auxData", "frameId")
|
99
|
-
@frame_id ||= frame_id # Remember the very first frame since it's the main one
|
100
|
-
|
101
|
-
if @frames[frame_id]
|
102
|
-
@frames[frame_id].merge!("execution_context_id" => context_id)
|
103
|
-
else
|
104
|
-
@frames[frame_id] = { "execution_context_id" => context_id }
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
on("Runtime.executionContextDestroyed") do |params|
|
109
|
-
context_id = params["executionContextId"]
|
110
|
-
|
111
|
-
if @execution_context_id == context_id
|
112
|
-
@execution_context_id = nil
|
113
|
-
end
|
114
|
-
|
115
|
-
_id, frame = @frames.find { |_, p| p["execution_context_id"] == context_id }
|
116
|
-
frame["execution_context_id"] = nil if frame
|
117
|
-
end
|
118
|
-
|
119
|
-
on("Runtime.executionContextsCleared") do
|
120
|
-
# If we didn't have time to set context id at the beginning we have
|
121
|
-
# to set lock and release it when we set something.
|
122
|
-
@execution_context_id = nil
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
def current_execution_context_id
|
127
|
-
if @frame_stack.empty?
|
128
|
-
@execution_context_id
|
129
|
-
else
|
130
|
-
@frames.dig(@frame_stack.last, "execution_context_id")
|
131
|
-
end
|
132
|
-
end
|
133
|
-
end
|
134
|
-
end
|
135
|
-
end
|