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