ferrum 0.15 → 0.17
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 +103 -14
- data/lib/ferrum/browser/command.rb +0 -4
- data/lib/ferrum/browser/options/base.rb +9 -0
- data/lib/ferrum/browser/options/chrome.rb +6 -10
- data/lib/ferrum/browser/options.rb +2 -1
- data/lib/ferrum/browser/process.rb +9 -11
- data/lib/ferrum/browser.rb +45 -2
- data/lib/ferrum/client/subscriber.rb +9 -3
- data/lib/ferrum/client/web_socket.rb +26 -5
- data/lib/ferrum/client.rb +12 -2
- data/lib/ferrum/context.rb +34 -11
- data/lib/ferrum/contexts.rb +20 -4
- data/lib/ferrum/cookies.rb +27 -0
- data/lib/ferrum/errors.rb +8 -2
- data/lib/ferrum/frame/dom.rb +18 -2
- data/lib/ferrum/frame.rb +17 -0
- data/lib/ferrum/keyboard.rb +0 -1
- data/lib/ferrum/mouse.rb +43 -7
- data/lib/ferrum/network/exchange.rb +33 -3
- data/lib/ferrum/network/request.rb +9 -0
- data/lib/ferrum/network.rb +52 -17
- data/lib/ferrum/node.rb +12 -0
- data/lib/ferrum/page/frames.rb +8 -6
- data/lib/ferrum/page/screencast.rb +102 -0
- data/lib/ferrum/page/screenshot.rb +19 -5
- data/lib/ferrum/page.rb +33 -2
- data/lib/ferrum/target.rb +10 -1
- data/lib/ferrum/utils/elapsed_time.rb +4 -0
- data/lib/ferrum/version.rb +1 -1
- metadata +18 -3
data/lib/ferrum/contexts.rb
CHANGED
@@ -4,6 +4,8 @@ require "ferrum/context"
|
|
4
4
|
|
5
5
|
module Ferrum
|
6
6
|
class Contexts
|
7
|
+
ALLOWED_TARGET_TYPES = %w[page iframe].freeze
|
8
|
+
|
7
9
|
include Enumerable
|
8
10
|
|
9
11
|
attr_reader :contexts
|
@@ -67,12 +69,19 @@ module Ferrum
|
|
67
69
|
|
68
70
|
private
|
69
71
|
|
72
|
+
# rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
70
73
|
def subscribe
|
71
74
|
@client.on("Target.attachedToTarget") do |params|
|
72
75
|
info, session_id = params.values_at("targetInfo", "sessionId")
|
73
|
-
next unless info["type"]
|
76
|
+
next unless ALLOWED_TARGET_TYPES.include?(info["type"])
|
74
77
|
|
75
78
|
context_id = info["browserContextId"]
|
79
|
+
unless @contexts[context_id]
|
80
|
+
context = Context.new(@client, self, context_id)
|
81
|
+
@contexts[context_id] = context
|
82
|
+
@default_context ||= context
|
83
|
+
end
|
84
|
+
|
76
85
|
@contexts[context_id]&.add_target(session_id: session_id, params: info)
|
77
86
|
if params["waitingForDebugger"]
|
78
87
|
@client.session(session_id).command("Runtime.runIfWaitingForDebugger", async: true)
|
@@ -81,15 +90,21 @@ module Ferrum
|
|
81
90
|
|
82
91
|
@client.on("Target.targetCreated") do |params|
|
83
92
|
info = params["targetInfo"]
|
84
|
-
next unless info["type"]
|
93
|
+
next unless ALLOWED_TARGET_TYPES.include?(info["type"])
|
85
94
|
|
86
95
|
context_id = info["browserContextId"]
|
87
|
-
|
96
|
+
|
97
|
+
if info["type"] == "iframe" &&
|
98
|
+
(target = @contexts[context_id].find_target { |t| t.connected? && t.page.frame_by(id: info["targetId"]) })
|
99
|
+
@contexts[context_id]&.add_target(session_id: target.page.client.session_id, params: info)
|
100
|
+
else
|
101
|
+
@contexts[context_id]&.add_target(params: info)
|
102
|
+
end
|
88
103
|
end
|
89
104
|
|
90
105
|
@client.on("Target.targetInfoChanged") do |params|
|
91
106
|
info = params["targetInfo"]
|
92
|
-
next unless info["type"]
|
107
|
+
next unless ALLOWED_TARGET_TYPES.include?(info["type"])
|
93
108
|
|
94
109
|
context_id, target_id = info.values_at("browserContextId", "targetId")
|
95
110
|
@contexts[context_id]&.update_target(target_id, info)
|
@@ -105,6 +120,7 @@ module Ferrum
|
|
105
120
|
context&.delete_target(params["targetId"])
|
106
121
|
end
|
107
122
|
end
|
123
|
+
# rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
108
124
|
|
109
125
|
def discover
|
110
126
|
@client.command("Target.setDiscoverTargets", discover: true)
|
data/lib/ferrum/cookies.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "yaml"
|
3
4
|
require "ferrum/cookies/cookie"
|
4
5
|
|
5
6
|
module Ferrum
|
@@ -169,6 +170,32 @@ module Ferrum
|
|
169
170
|
true
|
170
171
|
end
|
171
172
|
|
173
|
+
#
|
174
|
+
# Stores all cookies of current page in a file.
|
175
|
+
#
|
176
|
+
# @return [Integer]
|
177
|
+
#
|
178
|
+
# @example
|
179
|
+
# browser.cookies.store # => Integer
|
180
|
+
#
|
181
|
+
def store(path = "cookies.yml")
|
182
|
+
File.write(path, map(&:to_h).to_yaml)
|
183
|
+
end
|
184
|
+
|
185
|
+
#
|
186
|
+
# Loads all cookies from the file and sets them for current page.
|
187
|
+
#
|
188
|
+
# @return [true]
|
189
|
+
#
|
190
|
+
# @example
|
191
|
+
# browser.cookies.load # => true
|
192
|
+
#
|
193
|
+
def load(path = "cookies.yml")
|
194
|
+
cookies = YAML.load_file(path)
|
195
|
+
cookies.each { |c| set(c) }
|
196
|
+
true
|
197
|
+
end
|
198
|
+
|
172
199
|
private
|
173
200
|
|
174
201
|
def default_domain
|
data/lib/ferrum/errors.rb
CHANGED
@@ -78,6 +78,13 @@ module Ferrum
|
|
78
78
|
end
|
79
79
|
end
|
80
80
|
|
81
|
+
class InvalidScreenshotFormatError < Error
|
82
|
+
def initialize(format)
|
83
|
+
valid_formats = Page::Screenshot::SUPPORTED_SCREENSHOT_FORMAT.join(" | ")
|
84
|
+
super("Invalid value #{format} for option `:format` (#{valid_formats})")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
81
88
|
class BrowserError < Error
|
82
89
|
attr_reader :response
|
83
90
|
|
@@ -99,8 +106,7 @@ module Ferrum
|
|
99
106
|
|
100
107
|
class NoExecutionContextError < BrowserError
|
101
108
|
def initialize(response = nil)
|
102
|
-
response
|
103
|
-
super(response)
|
109
|
+
super(response || { "message" => "There's no context available" })
|
104
110
|
end
|
105
111
|
end
|
106
112
|
|
data/lib/ferrum/frame/dom.rb
CHANGED
@@ -14,7 +14,7 @@
|
|
14
14
|
# any updates, for example, the node may be destroyed without any notification.
|
15
15
|
# This is a way to keep a reference to the Node, when you don't necessarily want
|
16
16
|
# to keep track of it. One example would be linking to the node from performance
|
17
|
-
# data (e.g.
|
17
|
+
# data (e.g. re-layout root node). BackendNodeId may be either resolved to
|
18
18
|
# inspected node (DOM.pushNodesByBackendIdsToFrontend) or described in more
|
19
19
|
# details (DOM.describeNode).
|
20
20
|
module Ferrum
|
@@ -92,7 +92,23 @@ module Ferrum
|
|
92
92
|
# browser.body # => '<html itemscope="" itemtype="http://schema.org/WebPage" lang="ru"><head>...
|
93
93
|
#
|
94
94
|
def body
|
95
|
-
evaluate("document.documentElement
|
95
|
+
evaluate("document.documentElement?.outerHTML") || ""
|
96
|
+
end
|
97
|
+
|
98
|
+
#
|
99
|
+
# Returns the element in which the window is embedded.
|
100
|
+
#
|
101
|
+
# @return [Node, nil]
|
102
|
+
# The element in which the window is embedded.
|
103
|
+
#
|
104
|
+
# @example
|
105
|
+
# browser.go_to("https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe")
|
106
|
+
# frame = browser.frames.last
|
107
|
+
# frame.frame_element # => [Node]
|
108
|
+
# frame.parent.parent.parent.frame_element # => nil
|
109
|
+
#
|
110
|
+
def frame_element
|
111
|
+
evaluate("window.frameElement")
|
96
112
|
end
|
97
113
|
|
98
114
|
#
|
data/lib/ferrum/frame.rb
CHANGED
@@ -94,6 +94,23 @@ module Ferrum
|
|
94
94
|
@parent_id.nil?
|
95
95
|
end
|
96
96
|
|
97
|
+
#
|
98
|
+
# Returns the parent frame if this frame is nested in another one.
|
99
|
+
#
|
100
|
+
# @return [Frame, nil]
|
101
|
+
#
|
102
|
+
# @example
|
103
|
+
# browser.go_to("https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe")
|
104
|
+
# frame = browser.frames.last
|
105
|
+
# frame.url # => "https://www.openstreetmap.org/export/embed.html?bbox=-0.004017949104309083%2C51.47612752641776%2C0.00030577182769775396%2C51.478569861898606&layer=mapnik"
|
106
|
+
# frame.parent.main? # => false
|
107
|
+
# frame.parent.parent.main? # => false
|
108
|
+
# frame.parent.parent.parent.main? # => true
|
109
|
+
#
|
110
|
+
def parent
|
111
|
+
@page.frame_by(id: @parent_id) if @parent_id
|
112
|
+
end
|
113
|
+
|
97
114
|
#
|
98
115
|
# Sets a content of a given frame.
|
99
116
|
#
|
data/lib/ferrum/keyboard.rb
CHANGED
data/lib/ferrum/mouse.rb
CHANGED
@@ -3,11 +3,36 @@
|
|
3
3
|
module Ferrum
|
4
4
|
class Mouse
|
5
5
|
CLICK_WAIT = ENV.fetch("FERRUM_CLICK_WAIT", 0.1).to_f
|
6
|
-
|
6
|
+
BUTTON_MASKS = {
|
7
|
+
"none" => 0,
|
8
|
+
"left" => 1,
|
9
|
+
"right" => 2,
|
10
|
+
"middle" => 4,
|
11
|
+
"back" => 8,
|
12
|
+
"forward" => 16
|
13
|
+
}.freeze
|
7
14
|
|
8
15
|
def initialize(page)
|
9
16
|
@page = page
|
10
17
|
@x = @y = 0
|
18
|
+
@buttons = 0
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# Scroll page by the given amount x, y.
|
23
|
+
#
|
24
|
+
# @param [Integer] x
|
25
|
+
# The horizontal pixel value that you want to scroll by.
|
26
|
+
#
|
27
|
+
# @param [Integer] y
|
28
|
+
# The vertical pixel value that you want to scroll by.
|
29
|
+
#
|
30
|
+
# @example
|
31
|
+
# browser.go_to("https://www.google.com/search?q=Ruby+headless+driver+for+Capybara")
|
32
|
+
# browser.mouse.scroll_by(0, 400)
|
33
|
+
#
|
34
|
+
def scroll_by(x, y)
|
35
|
+
tap { @page.execute("window.scrollBy(#{x}, #{y})") }
|
11
36
|
end
|
12
37
|
|
13
38
|
#
|
@@ -107,9 +132,9 @@ module Ferrum
|
|
107
132
|
#
|
108
133
|
# Mouse move to given x and y.
|
109
134
|
#
|
110
|
-
# @param [
|
135
|
+
# @param [Number] x
|
111
136
|
#
|
112
|
-
# @param [
|
137
|
+
# @param [Number] y
|
113
138
|
#
|
114
139
|
# @param [Integer] steps
|
115
140
|
# Sends intermediate mousemove events.
|
@@ -129,8 +154,9 @@ module Ferrum
|
|
129
154
|
@page.command("Input.dispatchMouseEvent",
|
130
155
|
slowmoable: true,
|
131
156
|
type: "mouseMoved",
|
132
|
-
x: new_x
|
133
|
-
y: new_y
|
157
|
+
x: new_x,
|
158
|
+
y: new_y,
|
159
|
+
buttons: @buttons)
|
134
160
|
end
|
135
161
|
|
136
162
|
self
|
@@ -140,16 +166,26 @@ module Ferrum
|
|
140
166
|
|
141
167
|
def mouse_event(type:, button: :left, count: 1, modifiers: nil, wait: 0)
|
142
168
|
button = validate_button(button)
|
143
|
-
|
169
|
+
register_event_button(type, button)
|
170
|
+
options = { x: @x, y: @y, type: type, button: button, buttons: @buttons, clickCount: count }
|
144
171
|
options.merge!(modifiers: modifiers) if modifiers
|
145
172
|
@page.command("Input.dispatchMouseEvent", wait: wait, slowmoable: true, **options)
|
146
173
|
end
|
147
174
|
|
148
175
|
def validate_button(button)
|
149
176
|
button = button.to_s
|
150
|
-
raise "Invalid button: #{button}" unless
|
177
|
+
raise "Invalid button: #{button}" unless BUTTON_MASKS.key?(button)
|
151
178
|
|
152
179
|
button
|
153
180
|
end
|
181
|
+
|
182
|
+
def register_event_button(type, button)
|
183
|
+
case type
|
184
|
+
when "mousePressed"
|
185
|
+
@buttons |= BUTTON_MASKS[button]
|
186
|
+
when "mouseReleased"
|
187
|
+
@buttons &= ~BUTTON_MASKS[button]
|
188
|
+
end
|
189
|
+
end
|
154
190
|
end
|
155
191
|
end
|
@@ -28,6 +28,15 @@ module Ferrum
|
|
28
28
|
# @return [Error, nil]
|
29
29
|
attr_accessor :error
|
30
30
|
|
31
|
+
# Determines if the network exchange is unknown due to
|
32
|
+
# a lost of its context
|
33
|
+
#
|
34
|
+
# @return Boolean
|
35
|
+
attr_accessor :unknown
|
36
|
+
|
37
|
+
# @api private
|
38
|
+
attr_accessor :request_extra_info
|
39
|
+
|
31
40
|
#
|
32
41
|
# Initializes the network exchange.
|
33
42
|
#
|
@@ -40,6 +49,8 @@ module Ferrum
|
|
40
49
|
@page = page
|
41
50
|
@intercepted_request = nil
|
42
51
|
@request = @response = @error = nil
|
52
|
+
@request_extra_info = nil
|
53
|
+
@unknown = false
|
43
54
|
end
|
44
55
|
|
45
56
|
#
|
@@ -54,6 +65,15 @@ module Ferrum
|
|
54
65
|
request&.type?(:document) && request&.frame_id == frame_id
|
55
66
|
end
|
56
67
|
|
68
|
+
#
|
69
|
+
# The loader ID of the request.
|
70
|
+
#
|
71
|
+
# @return [String, nil]
|
72
|
+
#
|
73
|
+
def loader_id
|
74
|
+
request&.loader_id
|
75
|
+
end
|
76
|
+
|
57
77
|
#
|
58
78
|
# Determines if the network exchange has a request.
|
59
79
|
#
|
@@ -74,12 +94,12 @@ module Ferrum
|
|
74
94
|
|
75
95
|
#
|
76
96
|
# Determines if the request was blocked, a response was returned, or if an
|
77
|
-
# error occurred.
|
97
|
+
# error occurred or the exchange is unknown and cannot be inferred.
|
78
98
|
#
|
79
99
|
# @return [Boolean]
|
80
100
|
#
|
81
101
|
def finished?
|
82
|
-
blocked? || response&.loaded? || !error.nil? || ping?
|
102
|
+
blocked? || response&.loaded? || !error.nil? || ping? || blob? || unknown
|
83
103
|
end
|
84
104
|
|
85
105
|
#
|
@@ -127,6 +147,15 @@ module Ferrum
|
|
127
147
|
!!request&.ping?
|
128
148
|
end
|
129
149
|
|
150
|
+
#
|
151
|
+
# Determines if the exchange is blob.
|
152
|
+
#
|
153
|
+
# @return [Boolean]
|
154
|
+
#
|
155
|
+
def blob?
|
156
|
+
!!url&.start_with?("blob:")
|
157
|
+
end
|
158
|
+
|
130
159
|
#
|
131
160
|
# Returns request's URL.
|
132
161
|
#
|
@@ -156,7 +185,8 @@ module Ferrum
|
|
156
185
|
"@intercepted_request=#{@intercepted_request.inspect} " \
|
157
186
|
"@request=#{@request.inspect} " \
|
158
187
|
"@response=#{@response.inspect} " \
|
159
|
-
"@error=#{@error.inspect}>"
|
188
|
+
"@error=#{@error.inspect}> " \
|
189
|
+
"@unknown=#{@unknown.inspect}>"
|
160
190
|
end
|
161
191
|
end
|
162
192
|
end
|
data/lib/ferrum/network.rb
CHANGED
@@ -41,7 +41,7 @@ module Ferrum
|
|
41
41
|
end
|
42
42
|
|
43
43
|
#
|
44
|
-
# Waits for network idle
|
44
|
+
# Waits for network idle.
|
45
45
|
#
|
46
46
|
# @param [Integer] connections
|
47
47
|
# how many connections are allowed for network to be idling,
|
@@ -52,21 +52,33 @@ module Ferrum
|
|
52
52
|
# @param [Float] timeout
|
53
53
|
# During what time we try to check idle.
|
54
54
|
#
|
55
|
-
# @
|
55
|
+
# @return [Boolean]
|
56
56
|
#
|
57
57
|
# @example
|
58
58
|
# browser.go_to("https://example.com/")
|
59
59
|
# browser.at_xpath("//a[text() = 'No UI changes button']").click
|
60
|
-
# browser.network.wait_for_idle
|
60
|
+
# browser.network.wait_for_idle # => false
|
61
61
|
#
|
62
62
|
def wait_for_idle(connections: 0, duration: 0.05, timeout: @page.timeout)
|
63
63
|
start = Utils::ElapsedTime.monotonic_time
|
64
64
|
|
65
65
|
until idle?(connections)
|
66
|
-
|
66
|
+
return false if Utils::ElapsedTime.timeout?(start, timeout)
|
67
67
|
|
68
68
|
sleep(duration)
|
69
69
|
end
|
70
|
+
|
71
|
+
true
|
72
|
+
end
|
73
|
+
|
74
|
+
#
|
75
|
+
# Waits for network idle or raises {Ferrum::TimeoutError} error.
|
76
|
+
# Accepts same arguments as `wait_for_idle`.
|
77
|
+
#
|
78
|
+
# @raise [Ferrum::TimeoutError]
|
79
|
+
def wait_for_idle!(...)
|
80
|
+
result = wait_for_idle(...)
|
81
|
+
raise TimeoutError unless result
|
70
82
|
end
|
71
83
|
|
72
84
|
def idle?(connections = 0)
|
@@ -88,7 +100,7 @@ module Ferrum
|
|
88
100
|
#
|
89
101
|
# Page request of the main frame.
|
90
102
|
#
|
91
|
-
# @return [Request]
|
103
|
+
# @return [Request, nil]
|
92
104
|
#
|
93
105
|
# @example
|
94
106
|
# browser.go_to("https://github.com/")
|
@@ -356,29 +368,38 @@ module Ferrum
|
|
356
368
|
@page.on("Network.requestWillBeSent") do |params|
|
357
369
|
request = Network::Request.new(params)
|
358
370
|
|
359
|
-
# We can build exchange in two places, here on the event or when request
|
360
|
-
# is interrupted. So we have to be careful when to create new one. We
|
361
|
-
# create new exchange only if there's no with such id or there's but
|
362
|
-
# it's filled with request which means this one is new but has response
|
363
|
-
# for a redirect. So we assign response from the params to previous
|
364
|
-
# exchange and build new exchange to assign this request to it.
|
365
|
-
exchange = select(request.id).last
|
366
|
-
exchange = build_exchange(request.id) unless exchange&.blank?
|
367
|
-
|
368
371
|
# On redirects Chrome doesn't change `requestId` and there's no
|
369
372
|
# `Network.responseReceived` event for such request. If there's already
|
370
373
|
# exchange object with this id then we got redirected and params has
|
371
374
|
# `redirectResponse` key which contains the response.
|
372
|
-
if params["redirectResponse"]
|
373
|
-
previous_exchange = select(request.id)[-2]
|
375
|
+
if params["redirectResponse"] && (previous_exchange = select(request.id).last)
|
374
376
|
response = Network::Response.new(@page, params)
|
375
377
|
response.loaded = true
|
376
378
|
previous_exchange.response = response
|
377
379
|
end
|
378
380
|
|
381
|
+
# We can build exchange in two places, here on the event or when request
|
382
|
+
# is interrupted. So we have to be careful when to create new one. We
|
383
|
+
# create new exchange only if there's no with such id or there's, but
|
384
|
+
# it's filled with request which means this one is new but has response
|
385
|
+
# for a redirect. So we assign response from the params to previous
|
386
|
+
# exchange and build new exchange to assign this request to it.
|
387
|
+
exchange = select(request.id).last
|
388
|
+
exchange = build_exchange(request.id) if exchange.nil? || !exchange.blank?
|
389
|
+
request.headers.merge!(Hash(exchange.request_extra_info&.dig("headers")))
|
379
390
|
exchange.request = request
|
380
391
|
|
381
|
-
|
392
|
+
if exchange.navigation_request?(@page.main_frame.id)
|
393
|
+
@exchange = exchange
|
394
|
+
classify_pending_exchanges(exchange.loader_id)
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
@page.on("Network.requestWillBeSentExtraInfo") do |params|
|
399
|
+
exchange = select(params["requestId"]).last
|
400
|
+
exchange ||= build_exchange(params["requestId"])
|
401
|
+
exchange.request_extra_info = params
|
402
|
+
exchange.request&.headers&.merge!(params["headers"])
|
382
403
|
end
|
383
404
|
end
|
384
405
|
|
@@ -397,6 +418,8 @@ module Ferrum
|
|
397
418
|
exchange = select(params["requestId"]).last
|
398
419
|
next unless exchange
|
399
420
|
|
421
|
+
exchange.unknown = false
|
422
|
+
|
400
423
|
if (response = exchange.response)
|
401
424
|
response.loaded = true
|
402
425
|
response.body_size = params["encodedDataLength"]
|
@@ -479,5 +502,17 @@ module Ferrum
|
|
479
502
|
def whitelist?
|
480
503
|
Array(@whitelist).any?
|
481
504
|
end
|
505
|
+
|
506
|
+
# When the main frame navigates Chrome doesn't send `Network.loadingFailed`
|
507
|
+
# for pending async requests. Therefore, we mark pending connections as unknown since
|
508
|
+
# they are not relevant to the current navigation.
|
509
|
+
def classify_pending_exchanges(new_loader_id)
|
510
|
+
@traffic.each do |exchange|
|
511
|
+
break if exchange.loader_id == new_loader_id
|
512
|
+
next unless exchange.pending?
|
513
|
+
|
514
|
+
exchange.unknown = true
|
515
|
+
end
|
516
|
+
end
|
482
517
|
end
|
483
518
|
end
|
data/lib/ferrum/node.rb
CHANGED
@@ -76,6 +76,7 @@ module Ferrum
|
|
76
76
|
when :double
|
77
77
|
page.mouse.move(x: x, y: y)
|
78
78
|
page.mouse.down(modifiers: modifiers, count: 2)
|
79
|
+
sleep(delay)
|
79
80
|
page.mouse.up(modifiers: modifiers, count: 2)
|
80
81
|
when :left
|
81
82
|
page.mouse.click(x: x, y: y, modifiers: modifiers, delay: delay)
|
@@ -217,6 +218,17 @@ module Ferrum
|
|
217
218
|
.each_with_object({}) { |style, memo| memo.merge!(style["name"] => style["value"]) }
|
218
219
|
end
|
219
220
|
|
221
|
+
def remove
|
222
|
+
page.command("DOM.removeNode", nodeId: node_id)
|
223
|
+
end
|
224
|
+
|
225
|
+
def exists?
|
226
|
+
page.command("DOM.resolveNode", nodeId: node_id)
|
227
|
+
true
|
228
|
+
rescue Ferrum::NodeNotFoundError
|
229
|
+
false
|
230
|
+
end
|
231
|
+
|
220
232
|
private
|
221
233
|
|
222
234
|
def bounding_rect_coordinates
|
data/lib/ferrum/page/frames.rb
CHANGED
@@ -36,7 +36,7 @@ module Ferrum
|
|
36
36
|
end
|
37
37
|
|
38
38
|
#
|
39
|
-
# Find frame by given
|
39
|
+
# Find a frame by given params.
|
40
40
|
#
|
41
41
|
# @param [String] id
|
42
42
|
# Unique frame's id that page provides.
|
@@ -60,8 +60,6 @@ module Ferrum
|
|
60
60
|
frames.find { |f| f.name == name }
|
61
61
|
elsif execution_id
|
62
62
|
frames.find { |f| f.execution_id == execution_id }
|
63
|
-
else
|
64
|
-
raise ArgumentError
|
65
63
|
end
|
66
64
|
end
|
67
65
|
|
@@ -126,11 +124,11 @@ module Ferrum
|
|
126
124
|
on("Page.frameStoppedLoading") do |params|
|
127
125
|
# `DOM.performSearch` doesn't work without getting #document node first.
|
128
126
|
# It returns node with nodeId 1 and nodeType 9 from which descend the
|
129
|
-
# tree and we save it in a variable because if we call that again root
|
127
|
+
# tree, and we save it in a variable because if we call that again root
|
130
128
|
# node will change the id and all subsequent nodes have to change id too.
|
131
129
|
if @main_frame.id == params["frameId"]
|
132
130
|
@event.set if idling?
|
133
|
-
document_node_id
|
131
|
+
document_node_id(async: true)
|
134
132
|
end
|
135
133
|
|
136
134
|
frame = @frames[params["frameId"]]
|
@@ -179,12 +177,16 @@ module Ferrum
|
|
179
177
|
execution_id = params["executionContextId"]
|
180
178
|
frame = frame_by(execution_id: execution_id)
|
181
179
|
frame&.execution_id = nil
|
180
|
+
frame&.state = :stopped_loading
|
182
181
|
end
|
183
182
|
end
|
184
183
|
|
185
184
|
def subscribe_execution_contexts_cleared
|
186
185
|
on("Runtime.executionContextsCleared") do
|
187
|
-
@frames.each_value
|
186
|
+
@frames.each_value do |f|
|
187
|
+
f.execution_id = nil
|
188
|
+
f.state = :stopped_loading
|
189
|
+
end
|
188
190
|
end
|
189
191
|
end
|
190
192
|
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ferrum
|
4
|
+
class Page
|
5
|
+
module Screencast
|
6
|
+
# Starts sending frames to record screencast to the given block.
|
7
|
+
#
|
8
|
+
# @param [Hash{Symbol => Object}] opts
|
9
|
+
#
|
10
|
+
# @option opts [:jpeg, :png] :format
|
11
|
+
# The format the image should be returned in.
|
12
|
+
#
|
13
|
+
# @option opts [Integer] :quality
|
14
|
+
# The image quality. **Note:** 0-100 works for JPEG only.
|
15
|
+
#
|
16
|
+
# @option opts [Integer] :max_width
|
17
|
+
# Maximum screencast frame width.
|
18
|
+
#
|
19
|
+
# @option opts [Integer] :max_height
|
20
|
+
# Maximum screencast frame height.
|
21
|
+
#
|
22
|
+
# @option opts [Integer] :every_nth_frame
|
23
|
+
# Send every n-th frame.
|
24
|
+
#
|
25
|
+
# @yield [data, metadata, session_id]
|
26
|
+
# The given block receives the screencast frame along with metadata
|
27
|
+
# about the frame and the screencast session ID.
|
28
|
+
#
|
29
|
+
# @yieldparam data [String]
|
30
|
+
# Base64-encoded compressed image.
|
31
|
+
#
|
32
|
+
# @yieldparam metadata [Hash{String => Object}]
|
33
|
+
# Screencast frame metadata.
|
34
|
+
#
|
35
|
+
# @option metadata [Integer] 'offsetTop'
|
36
|
+
# Top offset in DIP.
|
37
|
+
#
|
38
|
+
# @option metadata [Integer] 'pageScaleFactor'
|
39
|
+
# Page scale factor.
|
40
|
+
#
|
41
|
+
# @option metadata [Integer] 'deviceWidth'
|
42
|
+
# Device screen width in DIP.
|
43
|
+
#
|
44
|
+
# @option metadata [Integer] 'deviceHeight'
|
45
|
+
# Device screen height in DIP.
|
46
|
+
#
|
47
|
+
# @option metadata [Integer] 'scrollOffsetX'
|
48
|
+
# Position of horizontal scroll in CSS pixels.
|
49
|
+
#
|
50
|
+
# @option metadata [Integer] 'scrollOffsetY'
|
51
|
+
# Position of vertical scroll in CSS pixels.
|
52
|
+
#
|
53
|
+
# @option metadata [Float] 'timestamp'
|
54
|
+
# (optional) Frame swap timestamp in seconds since Unix epoch.
|
55
|
+
#
|
56
|
+
# @yieldparam session_id [Integer]
|
57
|
+
# Frame number.
|
58
|
+
#
|
59
|
+
# @example
|
60
|
+
# require "base64"
|
61
|
+
#
|
62
|
+
# page.go_to("https://apple.com/ipad")
|
63
|
+
#
|
64
|
+
# page.start_screencast(format: :jpeg, quality: 75) do |data, metadata|
|
65
|
+
# timestamp = (metadata['timestamp'] * 1000).to_i
|
66
|
+
# File.binwrite("image_#{timestamp}.jpg", Base64.decode64(data))
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# sleep 10
|
70
|
+
#
|
71
|
+
# page.stop_screencast
|
72
|
+
#
|
73
|
+
def start_screencast(**opts)
|
74
|
+
options = opts.transform_keys { START_SCREENCAST_KEY_CONV.fetch(_1, _1) }
|
75
|
+
response = command("Page.startScreencast", **options)
|
76
|
+
|
77
|
+
if (error_text = response["errorText"]) # https://cs.chromium.org/chromium/src/net/base/net_error_list.h
|
78
|
+
raise "Starting screencast failed (#{error_text})"
|
79
|
+
end
|
80
|
+
|
81
|
+
on("Page.screencastFrame") do |params|
|
82
|
+
data, metadata, session_id = params.values_at("data", "metadata", "sessionId")
|
83
|
+
|
84
|
+
command("Page.screencastFrameAck", sessionId: session_id)
|
85
|
+
|
86
|
+
yield data, metadata, session_id
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Stops sending frames.
|
91
|
+
def stop_screencast
|
92
|
+
command("Page.stopScreencast")
|
93
|
+
end
|
94
|
+
|
95
|
+
START_SCREENCAST_KEY_CONV = {
|
96
|
+
max_width: :maxWidth,
|
97
|
+
max_height: :maxHeight,
|
98
|
+
every_nth_frame: :everyNthFrame
|
99
|
+
}.freeze
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|