ferrum 0.1.2 → 0.2
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 +513 -10
- data/lib/ferrum.rb +85 -4
- data/lib/ferrum/browser.rb +16 -33
- data/lib/ferrum/browser/client.rb +18 -2
- data/lib/ferrum/browser/subscriber.rb +5 -1
- data/lib/ferrum/cookies.rb +97 -0
- data/lib/ferrum/headers.rb +50 -0
- data/lib/ferrum/{page/input.json → keyboard.json} +0 -0
- data/lib/ferrum/keyboard.rb +119 -0
- data/lib/ferrum/mouse.rb +53 -0
- data/lib/ferrum/network/intercepted_request.rb +53 -0
- data/lib/ferrum/node.rb +52 -115
- data/lib/ferrum/page.rb +30 -30
- data/lib/ferrum/page/dom.rb +22 -12
- data/lib/ferrum/page/frame.rb +13 -12
- data/lib/ferrum/page/input.rb +3 -148
- data/lib/ferrum/page/net.rb +66 -54
- data/lib/ferrum/page/runtime.rb +13 -19
- data/lib/ferrum/page/screenshot.rb +84 -0
- data/lib/ferrum/targets.rb +4 -8
- data/lib/ferrum/version.rb +1 -1
- metadata +9 -10
- data/lib/ferrum/browser/api.rb +0 -14
- data/lib/ferrum/browser/api/cookie.rb +0 -46
- data/lib/ferrum/browser/api/header.rb +0 -32
- data/lib/ferrum/browser/api/intercept.rb +0 -32
- data/lib/ferrum/browser/api/screenshot.rb +0 -78
- data/lib/ferrum/cookie.rb +0 -47
- data/lib/ferrum/errors.rb +0 -94
data/lib/ferrum/mouse.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ferrum
|
4
|
+
class Mouse
|
5
|
+
VALID_BUTTONS = %w[none left middle right back forward].freeze
|
6
|
+
|
7
|
+
def initialize(page)
|
8
|
+
@page = page
|
9
|
+
@x = @y = 0
|
10
|
+
end
|
11
|
+
|
12
|
+
def click(x:, y:, delay: 0, timeout: 0, **options)
|
13
|
+
move(x: x, y: y)
|
14
|
+
down(**options)
|
15
|
+
sleep(delay)
|
16
|
+
# Potential wait because if network event is triggered then we have to wait until it's over.
|
17
|
+
up(timeout: timeout, **options)
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def down(**options)
|
22
|
+
tap { mouse_event(type: "mousePressed", **options) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def up(**options)
|
26
|
+
tap { mouse_event(type: "mouseReleased", **options) }
|
27
|
+
end
|
28
|
+
|
29
|
+
# FIXME: steps
|
30
|
+
def move(x:, y:, steps: 1)
|
31
|
+
@x, @y = x, y
|
32
|
+
@page.command("Input.dispatchMouseEvent", type: "mouseMoved", x: @x, y: @y)
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def mouse_event(type:, button: :left, count: 1, modifiers: nil, timeout: 0)
|
39
|
+
button = validate_button(button)
|
40
|
+
options = { x: @x, y: @y, type: type, button: button, clickCount: count }
|
41
|
+
options.merge!(modifiers: modifiers) if modifiers
|
42
|
+
@page.command("Input.dispatchMouseEvent", timeout: timeout, **options)
|
43
|
+
end
|
44
|
+
|
45
|
+
def validate_button(button)
|
46
|
+
button = button.to_s
|
47
|
+
unless VALID_BUTTONS.include?(button)
|
48
|
+
raise "Invalid button: #{button}"
|
49
|
+
end
|
50
|
+
button
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ferrum::Network
|
4
|
+
class InterceptedRequest
|
5
|
+
attr_accessor :interception_id, :frame_id, :resource_type,
|
6
|
+
:is_navigation_request
|
7
|
+
|
8
|
+
def initialize(page, params)
|
9
|
+
@page, @params = page, params
|
10
|
+
@interception_id = params["interceptionId"]
|
11
|
+
@frame_id = params["frameId"]
|
12
|
+
@resource_type = params["resourceType"]
|
13
|
+
@is_navigation_request = params["isNavigationRequest"]
|
14
|
+
@request = params.dig("request")
|
15
|
+
end
|
16
|
+
|
17
|
+
def auth_challenge?(source)
|
18
|
+
@params.dig("authChallenge", "source")&.downcase&.to_s == source.to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
def match?(regexp)
|
22
|
+
url.match?(regexp)
|
23
|
+
end
|
24
|
+
|
25
|
+
def abort
|
26
|
+
@page.abort_request(interception_id)
|
27
|
+
end
|
28
|
+
|
29
|
+
def continue(**options)
|
30
|
+
@page.continue_request(interception_id, **options)
|
31
|
+
end
|
32
|
+
|
33
|
+
def url
|
34
|
+
@request["url"]
|
35
|
+
end
|
36
|
+
|
37
|
+
def method
|
38
|
+
@request["method"]
|
39
|
+
end
|
40
|
+
|
41
|
+
def headers
|
42
|
+
@request["headers"]
|
43
|
+
end
|
44
|
+
|
45
|
+
def initial_priority
|
46
|
+
@request["initialPriority"]
|
47
|
+
end
|
48
|
+
|
49
|
+
def referrer_policy
|
50
|
+
@request["referrerPolicy"]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/ferrum/node.rb
CHANGED
@@ -2,153 +2,99 @@
|
|
2
2
|
|
3
3
|
module Ferrum
|
4
4
|
class Node
|
5
|
-
attr_reader :page, :target_id, :node_id, :description
|
5
|
+
attr_reader :page, :target_id, :node_id, :description, :tag_name
|
6
6
|
|
7
7
|
def initialize(page, target_id, node_id, description)
|
8
|
-
@page, @target_id
|
9
|
-
|
8
|
+
@page, @target_id = page, target_id
|
9
|
+
@node_id, @description = node_id, description
|
10
|
+
@tag_name = description["nodeName"].downcase
|
10
11
|
end
|
11
12
|
|
12
13
|
def node?
|
13
14
|
description["nodeType"] == 1 # nodeType: 3, nodeName: "#text" e.g.
|
14
15
|
end
|
15
16
|
|
16
|
-
def
|
17
|
-
page.
|
18
|
-
rescue BrowserError => e
|
19
|
-
case e.message
|
20
|
-
when "No node with given id found"
|
21
|
-
raise ObsoleteNode.new(self, e.response)
|
22
|
-
else
|
23
|
-
raise
|
24
|
-
end
|
17
|
+
def focus
|
18
|
+
tap { page.command("DOM.focus", nodeId: node_id) }
|
25
19
|
end
|
26
20
|
|
27
|
-
def
|
28
|
-
|
21
|
+
def blur
|
22
|
+
tap { evaluate("this.blur()") }
|
29
23
|
end
|
30
24
|
|
31
|
-
def
|
32
|
-
page.
|
25
|
+
def type(*keys)
|
26
|
+
tap { page.keyboard.type(*keys) }
|
33
27
|
end
|
34
28
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
page.
|
41
|
-
end
|
29
|
+
# mode: (:left | :right | :double)
|
30
|
+
# keys: (:alt, (:ctrl | :control), (:meta | :command), :shift)
|
31
|
+
# offset: { :x, :y }
|
32
|
+
def click(mode: :left, keys: [], offset: {})
|
33
|
+
x, y = page.find_position(self, offset[:x], offset[:y])
|
34
|
+
modifiers = page.keyboard.modifiers(keys)
|
42
35
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
if ((tag_name == "img") && (name == "src")) || ((tag_name == "a") && (name == "href"))
|
55
|
-
# if attribute exists get the property
|
56
|
-
return page_send(:attribute, name) && page_send(:property, name)
|
36
|
+
case mode
|
37
|
+
when :right
|
38
|
+
page.mouse.move(x: x, y: y)
|
39
|
+
page.mouse.down(button: :right, modifiers: modifiers)
|
40
|
+
page.mouse.up(button: :right, modifiers: modifiers)
|
41
|
+
when :double
|
42
|
+
page.mouse.move(x: x, y: y)
|
43
|
+
page.mouse.down(modifiers: modifiers, count: 2)
|
44
|
+
page.mouse.up(modifiers: modifiers, count: 2)
|
45
|
+
when :left
|
46
|
+
page.mouse.click(x: x, y: y, modifiers: modifiers, timeout: 0.05)
|
57
47
|
end
|
58
48
|
|
59
|
-
|
60
|
-
value = page_send(:attribute, name) if value.nil? || value.is_a?(Hash)
|
61
|
-
|
62
|
-
value
|
63
|
-
end
|
64
|
-
|
65
|
-
def attributes
|
66
|
-
page_send(:attributes)
|
67
|
-
end
|
68
|
-
|
69
|
-
def value
|
70
|
-
page.evaluate_on(node: self, expression: "this.value")
|
71
|
-
end
|
72
|
-
|
73
|
-
def set(value)
|
74
|
-
if tag_name == "input"
|
75
|
-
case self[:type]
|
76
|
-
when "radio"
|
77
|
-
click
|
78
|
-
when "checkbox"
|
79
|
-
click if value != checked?
|
80
|
-
when "file"
|
81
|
-
files = value.respond_to?(:to_ary) ? value.to_ary.map(&:to_s) : value.to_s
|
82
|
-
page_send(:select_file, files)
|
83
|
-
else
|
84
|
-
page_send(:set, value.to_s)
|
85
|
-
end
|
86
|
-
elsif tag_name == "textarea"
|
87
|
-
page_send(:set, value.to_s)
|
88
|
-
elsif self[:isContentEditable]
|
89
|
-
# FIXME:
|
90
|
-
page_send(:delete_text)
|
91
|
-
send_keys(value.to_s)
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
def select_option
|
96
|
-
page_send(:select, true)
|
49
|
+
self
|
97
50
|
end
|
98
51
|
|
99
|
-
def
|
100
|
-
raise
|
52
|
+
def hover
|
53
|
+
raise NotImplementedError
|
101
54
|
end
|
102
55
|
|
103
|
-
def
|
104
|
-
|
56
|
+
def trigger(event)
|
57
|
+
raise NotImplementedError
|
105
58
|
end
|
106
59
|
|
107
|
-
def
|
108
|
-
|
60
|
+
def select_file(value)
|
61
|
+
page.command("DOM.setFileInputFiles", nodeId: node_id, files: Array(value))
|
109
62
|
end
|
110
63
|
|
111
|
-
def
|
112
|
-
self
|
64
|
+
def at_xpath(selector)
|
65
|
+
page.at_xpath(selector, within: self)
|
113
66
|
end
|
114
67
|
|
115
|
-
def
|
116
|
-
|
68
|
+
def at_css(selector)
|
69
|
+
page.at_css(selector, within: self)
|
117
70
|
end
|
118
71
|
|
119
|
-
def
|
120
|
-
|
72
|
+
def xpath(selector)
|
73
|
+
page.xpath(selector, within: self)
|
121
74
|
end
|
122
75
|
|
123
|
-
def
|
124
|
-
|
76
|
+
def css(selector)
|
77
|
+
page.css(selector, within: self)
|
125
78
|
end
|
126
79
|
|
127
|
-
def
|
128
|
-
|
80
|
+
def text
|
81
|
+
evaluate("this.textContent")
|
129
82
|
end
|
130
83
|
|
131
|
-
def
|
132
|
-
|
84
|
+
def value
|
85
|
+
evaluate("this.value")
|
133
86
|
end
|
134
87
|
|
135
|
-
def
|
136
|
-
|
88
|
+
def property(name)
|
89
|
+
evaluate("this['#{name}']")
|
137
90
|
end
|
138
91
|
|
139
|
-
def
|
140
|
-
|
92
|
+
def attribute(name)
|
93
|
+
evaluate("this.getAttribute('#{name}')")
|
141
94
|
end
|
142
95
|
|
143
|
-
def
|
144
|
-
|
145
|
-
scroll_element_to_location(element, location)
|
146
|
-
elsif location.is_a?(Symbol)
|
147
|
-
scroll_to_location(location)
|
148
|
-
else
|
149
|
-
scroll_to_coords(*position)
|
150
|
-
end
|
151
|
-
self
|
96
|
+
def evaluate(expression)
|
97
|
+
page.evaluate_on(node: self, expression: expression)
|
152
98
|
end
|
153
99
|
|
154
100
|
def ==(other)
|
@@ -159,15 +105,6 @@ module Ferrum
|
|
159
105
|
target_id == other.target_id && description["backendNodeId"] == other.description["backendNodeId"]
|
160
106
|
end
|
161
107
|
|
162
|
-
def send_keys(*keys)
|
163
|
-
page_send(:send_keys, keys)
|
164
|
-
end
|
165
|
-
alias_method :send_key, :send_keys
|
166
|
-
|
167
|
-
def path
|
168
|
-
page_send(:path)
|
169
|
-
end
|
170
|
-
|
171
108
|
def inspect
|
172
109
|
%(#<#{self.class} @target_id=#{@target_id.inspect} @node_id=#{@node_id} @description=#{@description.inspect}>)
|
173
110
|
end
|
data/lib/ferrum/page.rb
CHANGED
@@ -1,14 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "ferrum/mouse"
|
4
|
+
require "ferrum/keyboard"
|
5
|
+
require "ferrum/headers"
|
6
|
+
require "ferrum/cookies"
|
3
7
|
require "ferrum/page/dom"
|
4
8
|
require "ferrum/page/input"
|
5
9
|
require "ferrum/page/runtime"
|
6
10
|
require "ferrum/page/frame"
|
7
11
|
require "ferrum/page/net"
|
12
|
+
require "ferrum/page/screenshot"
|
8
13
|
require "ferrum/browser/client"
|
9
14
|
require "ferrum/network/error"
|
10
15
|
require "ferrum/network/request"
|
11
16
|
require "ferrum/network/response"
|
17
|
+
require "ferrum/network/intercepted_request"
|
12
18
|
|
13
19
|
# RemoteObjectId is from a JavaScript world, and corresponds to any JavaScript
|
14
20
|
# object, including JS wrappers for DOM nodes. There is a way to convert between
|
@@ -31,10 +37,13 @@ module Ferrum
|
|
31
37
|
class Page
|
32
38
|
NEW_WINDOW_BUG_SLEEP = 0.3
|
33
39
|
|
34
|
-
include Input, DOM, Runtime, Frame, Net
|
40
|
+
include Input, DOM, Runtime, Frame, Net, Screenshot
|
35
41
|
|
36
42
|
attr_accessor :referrer
|
37
|
-
attr_reader :target_id, :status,
|
43
|
+
attr_reader :target_id, :status,
|
44
|
+
:headers, :cookies, :response_headers,
|
45
|
+
:mouse, :keyboard,
|
46
|
+
:browser
|
38
47
|
|
39
48
|
def initialize(target_id, browser, new_window = false)
|
40
49
|
@target_id, @browser = target_id, browser
|
@@ -50,22 +59,16 @@ module Ferrum
|
|
50
59
|
# Dirty hack because new window doesn't have events at all
|
51
60
|
sleep(NEW_WINDOW_BUG_SLEEP) if new_window
|
52
61
|
|
53
|
-
|
54
|
-
@session_id = @browser.command("Target.attachToTarget", targetId: @target_id)["sessionId"]
|
55
|
-
rescue BrowserError => e
|
56
|
-
case e.message
|
57
|
-
when "No target with given id found"
|
58
|
-
raise NoSuchWindowError
|
59
|
-
else
|
60
|
-
raise
|
61
|
-
end
|
62
|
-
end
|
62
|
+
@session_id = @browser.command("Target.attachToTarget", targetId: @target_id)["sessionId"]
|
63
63
|
|
64
64
|
host = @browser.process.host
|
65
65
|
port = @browser.process.port
|
66
66
|
ws_url = "ws://#{host}:#{port}/devtools/page/#{@target_id}"
|
67
67
|
@client = Browser::Client.new(browser, ws_url, 1000)
|
68
68
|
|
69
|
+
@mouse, @keyboard = Mouse.new(self), Keyboard.new(self)
|
70
|
+
@headers, @cookies = Headers.new(self), Cookies.new(self)
|
71
|
+
|
69
72
|
subscribe
|
70
73
|
prepare_page
|
71
74
|
end
|
@@ -83,12 +86,13 @@ module Ferrum
|
|
83
86
|
net::ERR_NAME_RESOLUTION_FAILED
|
84
87
|
net::ERR_INTERNET_DISCONNECTED
|
85
88
|
net::ERR_CONNECTION_TIMED_OUT].include?(response["errorText"])
|
86
|
-
raise
|
89
|
+
raise StatusError, options[:url]
|
87
90
|
end
|
88
91
|
response["frameId"]
|
89
92
|
end
|
90
93
|
|
91
94
|
def close
|
95
|
+
@headers.clear
|
92
96
|
@browser.command("Target.detachFromTarget", sessionId: @session_id)
|
93
97
|
@browser.command("Target.closeTarget", targetId: @target_id)
|
94
98
|
close_connection
|
@@ -130,12 +134,12 @@ module Ferrum
|
|
130
134
|
@network_traffic = []
|
131
135
|
end
|
132
136
|
|
133
|
-
def
|
134
|
-
|
137
|
+
def back
|
138
|
+
history_navigate(delta: -1)
|
135
139
|
end
|
136
140
|
|
137
|
-
def
|
138
|
-
|
141
|
+
def forward
|
142
|
+
history_navigate(delta: 1)
|
139
143
|
end
|
140
144
|
|
141
145
|
def accept_confirm
|
@@ -165,8 +169,8 @@ module Ferrum
|
|
165
169
|
|
166
170
|
begin
|
167
171
|
modal_text = @modal_messages.shift
|
168
|
-
raise
|
169
|
-
rescue
|
172
|
+
raise ModalNotFoundError if modal_text.nil? || (expect_text && !modal_text.match(expect_regexp))
|
173
|
+
rescue ModalNotFoundError => e
|
170
174
|
raise e, not_found_msg if (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start_time) >= timeout_sec
|
171
175
|
sleep(0.05)
|
172
176
|
retry
|
@@ -217,13 +221,13 @@ module Ferrum
|
|
217
221
|
options = { accept: accept_modal }
|
218
222
|
response = @modal_response || params["defaultPrompt"]
|
219
223
|
options.merge!(promptText: response) if response
|
220
|
-
|
224
|
+
command("Page.handleJavaScriptDialog", **options)
|
221
225
|
else
|
222
226
|
warn "Modal window has been opened, but you didn't wrap your code into (`accept_prompt` | `dismiss_prompt` | `accept_confirm` | `dismiss_confirm` | `accept_alert`), accepting by default"
|
223
227
|
options = { accept: true }
|
224
228
|
response = params["defaultPrompt"]
|
225
229
|
options.merge!(promptText: response) if response
|
226
|
-
|
230
|
+
command("Page.handleJavaScriptDialog", **options)
|
227
231
|
end
|
228
232
|
end
|
229
233
|
|
@@ -310,7 +314,7 @@ module Ferrum
|
|
310
314
|
end
|
311
315
|
|
312
316
|
@browser.extensions.each do |extension|
|
313
|
-
|
317
|
+
command("Page.addScriptToEvaluateOnNewDocument", source: extension)
|
314
318
|
end
|
315
319
|
|
316
320
|
inject_extensions
|
@@ -318,10 +322,6 @@ module Ferrum
|
|
318
322
|
width, height = @browser.window_size
|
319
323
|
resize(width: width, height: height)
|
320
324
|
|
321
|
-
url_whitelist = Array(@browser.url_whitelist)
|
322
|
-
url_blacklist = Array(@browser.url_blacklist)
|
323
|
-
intercept_request("*") if !url_whitelist.empty? || !url_blacklist.empty?
|
324
|
-
|
325
325
|
response = command("Page.getNavigationHistory")
|
326
326
|
if response.dig("entries", 0, "transitionType") != "typed"
|
327
327
|
# If we create page by clicking links, submiting forms and so on it
|
@@ -340,13 +340,13 @@ module Ferrum
|
|
340
340
|
# https://github.com/cyrus-and/chrome-remote-interface/issues/319
|
341
341
|
# We also evaluate script just in case because
|
342
342
|
# `Page.addScriptToEvaluateOnNewDocument` doesn't work in popups.
|
343
|
-
|
344
|
-
|
345
|
-
|
343
|
+
command("Runtime.evaluate", expression: extension,
|
344
|
+
contextId: execution_context_id,
|
345
|
+
returnByValue: true)
|
346
346
|
end
|
347
347
|
end
|
348
348
|
|
349
|
-
def
|
349
|
+
def history_navigate(delta:)
|
350
350
|
history = command("Page.getNavigationHistory")
|
351
351
|
index, entries = history.values_at("currentIndex", "entries")
|
352
352
|
|