ferrum 0.11 → 0.13
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +174 -30
- data/lib/ferrum/browser/binary.rb +46 -0
- data/lib/ferrum/browser/client.rb +17 -16
- data/lib/ferrum/browser/command.rb +10 -12
- data/lib/ferrum/browser/options/base.rb +2 -11
- data/lib/ferrum/browser/options/chrome.rb +29 -18
- data/lib/ferrum/browser/options/firefox.rb +13 -9
- data/lib/ferrum/browser/options.rb +84 -0
- data/lib/ferrum/browser/process.rb +45 -40
- data/lib/ferrum/browser/subscriber.rb +1 -3
- data/lib/ferrum/browser/version_info.rb +71 -0
- data/lib/ferrum/browser/web_socket.rb +9 -12
- data/lib/ferrum/browser/xvfb.rb +4 -8
- data/lib/ferrum/browser.rb +193 -47
- data/lib/ferrum/context.rb +9 -4
- data/lib/ferrum/contexts.rb +12 -10
- data/lib/ferrum/cookies/cookie.rb +126 -0
- data/lib/ferrum/cookies.rb +93 -55
- data/lib/ferrum/dialog.rb +30 -0
- data/lib/ferrum/errors.rb +115 -0
- data/lib/ferrum/frame/dom.rb +177 -0
- data/lib/ferrum/frame/runtime.rb +58 -75
- data/lib/ferrum/frame.rb +118 -23
- data/lib/ferrum/headers.rb +30 -2
- data/lib/ferrum/keyboard.rb +56 -13
- data/lib/ferrum/mouse.rb +92 -7
- data/lib/ferrum/network/auth_request.rb +7 -2
- data/lib/ferrum/network/exchange.rb +97 -12
- data/lib/ferrum/network/intercepted_request.rb +10 -8
- data/lib/ferrum/network/request.rb +69 -0
- data/lib/ferrum/network/response.rb +85 -3
- data/lib/ferrum/network.rb +285 -36
- data/lib/ferrum/node.rb +69 -23
- data/lib/ferrum/page/animation.rb +16 -1
- data/lib/ferrum/page/frames.rb +111 -30
- data/lib/ferrum/page/screenshot.rb +142 -65
- data/lib/ferrum/page/stream.rb +38 -0
- data/lib/ferrum/page/tracing.rb +97 -0
- data/lib/ferrum/page.rb +224 -60
- data/lib/ferrum/proxy.rb +147 -0
- data/lib/ferrum/{rbga.rb → rgba.rb} +4 -2
- data/lib/ferrum/target.rb +7 -4
- data/lib/ferrum/utils/attempt.rb +20 -0
- data/lib/ferrum/utils/elapsed_time.rb +27 -0
- data/lib/ferrum/utils/platform.rb +28 -0
- data/lib/ferrum/version.rb +1 -1
- data/lib/ferrum.rb +4 -146
- metadata +63 -51
data/lib/ferrum/frame.rb
CHANGED
@@ -5,37 +5,108 @@ require "ferrum/frame/runtime"
|
|
5
5
|
|
6
6
|
module Ferrum
|
7
7
|
class Frame
|
8
|
-
include DOM
|
8
|
+
include DOM
|
9
|
+
include Runtime
|
9
10
|
|
10
|
-
|
11
|
-
|
11
|
+
STATE_VALUES = %i[
|
12
|
+
started_loading
|
13
|
+
navigated
|
14
|
+
stopped_loading
|
15
|
+
].freeze
|
16
|
+
|
17
|
+
# The Frame's unique id.
|
18
|
+
#
|
19
|
+
# @return [String]
|
20
|
+
attr_accessor :id
|
21
|
+
|
22
|
+
# If frame was given a name it should be here.
|
23
|
+
#
|
24
|
+
# @return [String, nil]
|
25
|
+
attr_accessor :name
|
26
|
+
|
27
|
+
# The page the frame belongs to.
|
28
|
+
#
|
29
|
+
# @return [Page]
|
30
|
+
attr_reader :page
|
31
|
+
|
32
|
+
# Parent frame id if this one is nested in another one.
|
33
|
+
#
|
34
|
+
# @return [String, nil]
|
35
|
+
attr_reader :parent_id
|
36
|
+
|
37
|
+
# One of the states frame's in.
|
38
|
+
#
|
39
|
+
# @return [:started_loading, :navigated, :stopped_loading, nil]
|
40
|
+
attr_reader :state
|
12
41
|
|
13
42
|
def initialize(id, page, parent_id = nil)
|
14
|
-
@
|
15
|
-
@
|
43
|
+
@id = id
|
44
|
+
@page = page
|
45
|
+
@parent_id = parent_id
|
46
|
+
@execution_id = Concurrent::MVar.new
|
16
47
|
end
|
17
48
|
|
18
|
-
# Can be one of:
|
19
|
-
# * started_loading
|
20
|
-
# * navigated
|
21
|
-
# * stopped_loading
|
22
49
|
def state=(value)
|
50
|
+
raise ArgumentError unless STATE_VALUES.include?(value)
|
51
|
+
|
23
52
|
@state = value
|
24
53
|
end
|
25
54
|
|
55
|
+
#
|
56
|
+
# Returns current frame's `location.href`.
|
57
|
+
#
|
58
|
+
# @return [String]
|
59
|
+
#
|
60
|
+
# @example
|
61
|
+
# browser.go_to("https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe")
|
62
|
+
# frame = browser.frames[1]
|
63
|
+
# frame.url # => https://interactive-examples.mdn.mozilla.net/pages/tabbed/iframe.html
|
64
|
+
#
|
26
65
|
def url
|
27
66
|
evaluate("document.location.href")
|
28
67
|
end
|
29
68
|
|
69
|
+
#
|
70
|
+
# Returns current frame's title.
|
71
|
+
#
|
72
|
+
# @return [String]
|
73
|
+
#
|
74
|
+
# @example
|
75
|
+
# browser.go_to("https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe")
|
76
|
+
# frame = browser.frames[1]
|
77
|
+
# frame.title # => HTML Demo: <iframe>
|
78
|
+
#
|
30
79
|
def title
|
31
80
|
evaluate("document.title")
|
32
81
|
end
|
33
82
|
|
83
|
+
#
|
84
|
+
# If current frame is the main frame of the page (top of the tree).
|
85
|
+
#
|
86
|
+
# @return [Boolean]
|
87
|
+
#
|
88
|
+
# @example
|
89
|
+
# browser.go_to("https://www.w3schools.com/tags/tag_frame.asp")
|
90
|
+
# frame = browser.frame_by(id: "C09C4E4404314AAEAE85928EAC109A93")
|
91
|
+
# frame.main? # => false
|
92
|
+
#
|
34
93
|
def main?
|
35
94
|
@parent_id.nil?
|
36
95
|
end
|
37
96
|
|
38
|
-
|
97
|
+
#
|
98
|
+
# Sets a content of a given frame.
|
99
|
+
#
|
100
|
+
# @param [String] html
|
101
|
+
#
|
102
|
+
# @example
|
103
|
+
# browser.go_to("https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe")
|
104
|
+
# frame = browser.frames[1]
|
105
|
+
# frame.body # <html lang="en"><head><style>body {transition: opacity ease-in 0.2s; }...
|
106
|
+
# frame.content = "<html><head></head><body><p>lol</p></body></html>"
|
107
|
+
# frame.body # => <html><head></head><body><p>lol</p></body></html>
|
108
|
+
#
|
109
|
+
def content=(html)
|
39
110
|
evaluate_async(%(
|
40
111
|
document.open();
|
41
112
|
document.write(arguments[0]);
|
@@ -43,29 +114,53 @@ module Ferrum
|
|
43
114
|
arguments[1](true);
|
44
115
|
), @page.timeout, html)
|
45
116
|
end
|
117
|
+
alias set_content content=
|
46
118
|
|
47
|
-
|
48
|
-
|
119
|
+
#
|
120
|
+
# Execution context id which is used by JS, each frame has it's own
|
121
|
+
# context in which JS evaluates. Locks for a page timeout and raises
|
122
|
+
# an error if an execution id hasn't been set yet, if id is set
|
123
|
+
# returns immediately.
|
124
|
+
#
|
125
|
+
# @return [Integer]
|
126
|
+
#
|
127
|
+
# @raise [NoExecutionContextError]
|
128
|
+
#
|
129
|
+
def execution_id!
|
130
|
+
value = @execution_id.borrow(@page.timeout, &:itself)
|
131
|
+
raise NoExecutionContextError if value.instance_of?(Object)
|
132
|
+
|
133
|
+
value
|
49
134
|
end
|
50
135
|
|
136
|
+
#
|
137
|
+
# Execution context id which is used by JS, each frame has it's own
|
138
|
+
# context in which JS evaluates.
|
139
|
+
#
|
140
|
+
# @return [Integer, nil]
|
141
|
+
#
|
51
142
|
def execution_id
|
52
|
-
|
53
|
-
|
54
|
-
rescue NoExecutionContextError
|
55
|
-
@page.event.reset
|
56
|
-
@page.event.wait(@page.timeout) ? retry : raise
|
57
|
-
end
|
143
|
+
value = @execution_id.value
|
144
|
+
return if value.instance_of?(Object)
|
58
145
|
|
59
|
-
|
60
|
-
@execution_id ||= value
|
146
|
+
value
|
61
147
|
end
|
62
148
|
|
63
|
-
def
|
64
|
-
|
149
|
+
def execution_id=(value)
|
150
|
+
if value.nil?
|
151
|
+
@execution_id.try_take!
|
152
|
+
else
|
153
|
+
@execution_id.try_put!(value)
|
154
|
+
end
|
65
155
|
end
|
66
156
|
|
67
157
|
def inspect
|
68
|
-
|
158
|
+
"#<#{self.class} " \
|
159
|
+
"@id=#{@id.inspect} " \
|
160
|
+
"@parent_id=#{@parent_id.inspect} " \
|
161
|
+
"@name=#{@name.inspect} " \
|
162
|
+
"@state=#{@state.inspect} " \
|
163
|
+
"@execution_id=#{@execution_id.inspect}>"
|
69
164
|
end
|
70
165
|
end
|
71
166
|
end
|
data/lib/ferrum/headers.rb
CHANGED
@@ -7,20 +7,48 @@ module Ferrum
|
|
7
7
|
@headers = {}
|
8
8
|
end
|
9
9
|
|
10
|
+
#
|
11
|
+
# Get all headers.
|
12
|
+
#
|
13
|
+
# @return [Hash{String => String}]
|
14
|
+
#
|
10
15
|
def get
|
11
16
|
@headers
|
12
17
|
end
|
13
18
|
|
19
|
+
#
|
20
|
+
# Set given headers. Eventually clear all headers and set given ones.
|
21
|
+
#
|
22
|
+
# @param [Hash{String => String}] headers
|
23
|
+
# key-value pairs for example `"User-Agent" => "Browser"`.
|
24
|
+
#
|
25
|
+
# @return [true]
|
26
|
+
#
|
14
27
|
def set(headers)
|
15
28
|
clear
|
16
29
|
add(headers)
|
17
30
|
end
|
18
31
|
|
32
|
+
#
|
33
|
+
# Clear all headers.
|
34
|
+
#
|
35
|
+
# @return [true]
|
36
|
+
#
|
19
37
|
def clear
|
20
38
|
@headers = {}
|
21
39
|
true
|
22
40
|
end
|
23
41
|
|
42
|
+
#
|
43
|
+
# Adds given headers to already set ones.
|
44
|
+
#
|
45
|
+
# @param [Hash{String => String}] headers
|
46
|
+
# key-value pairs for example `"Referer" => "http://example.com"`.
|
47
|
+
#
|
48
|
+
# @param [Boolean] permanent
|
49
|
+
#
|
50
|
+
# @return [true]
|
51
|
+
#
|
24
52
|
def add(headers, permanent: true)
|
25
53
|
if headers["Referer"]
|
26
54
|
@page.referrer = headers["Referer"]
|
@@ -39,12 +67,12 @@ module Ferrum
|
|
39
67
|
private
|
40
68
|
|
41
69
|
def set_overrides(user_agent: nil, accept_language: nil, platform: nil)
|
42
|
-
options =
|
70
|
+
options = {}
|
43
71
|
options[:userAgent] = user_agent || @page.browser.default_user_agent
|
44
72
|
options[:acceptLanguage] = accept_language if accept_language
|
45
73
|
options[:platform] if platform
|
46
74
|
|
47
|
-
@page.command("Network.setUserAgentOverride", **options)
|
75
|
+
@page.command("Network.setUserAgentOverride", **options) unless options.empty?
|
48
76
|
end
|
49
77
|
end
|
50
78
|
end
|
data/lib/ferrum/keyboard.rb
CHANGED
@@ -4,14 +4,14 @@ require "json"
|
|
4
4
|
|
5
5
|
module Ferrum
|
6
6
|
class Keyboard
|
7
|
-
KEYS = JSON.parse(File.read(File.expand_path("
|
7
|
+
KEYS = JSON.parse(File.read(File.expand_path("keyboard.json", __dir__)))
|
8
8
|
MODIFIERS = { "alt" => 1, "ctrl" => 2, "control" => 2,
|
9
|
-
"meta" => 4, "command" => 4, "shift" => 8 }
|
9
|
+
"meta" => 4, "command" => 4, "shift" => 8 }.freeze
|
10
10
|
KEYS_MAPPING = {
|
11
11
|
cancel: "Cancel", help: "Help", backspace: "Backspace", tab: "Tab",
|
12
12
|
clear: "Clear", return: "Enter", enter: "Enter", shift: "Shift",
|
13
13
|
ctrl: "Control", control: "Control", alt: "Alt", pause: "Pause",
|
14
|
-
escape: "Escape", space: "Space",
|
14
|
+
escape: "Escape", space: "Space", pageup: "PageUp", page_up: "PageUp",
|
15
15
|
pagedown: "PageDown", page_down: "PageDown", end: "End", home: "Home",
|
16
16
|
left: "ArrowLeft", up: "ArrowUp", right: "ArrowRight",
|
17
17
|
down: "ArrowDown", insert: "Insert", delete: "Delete",
|
@@ -23,26 +23,51 @@ module Ferrum
|
|
23
23
|
separator: "NumpadDecimal", subtract: "NumpadSubtract",
|
24
24
|
decimal: "NumpadDecimal", divide: "NumpadDivide", f1: "F1", f2: "F2",
|
25
25
|
f3: "F3", f4: "F4", f5: "F5", f6: "F6", f7: "F7", f8: "F8", f9: "F9",
|
26
|
-
f10: "F10", f11: "F11", f12: "F12", meta: "Meta", command: "Meta"
|
27
|
-
}
|
26
|
+
f10: "F10", f11: "F11", f12: "F12", meta: "Meta", command: "Meta"
|
27
|
+
}.freeze
|
28
28
|
|
29
29
|
def initialize(page)
|
30
30
|
@page = page
|
31
31
|
end
|
32
32
|
|
33
|
+
#
|
34
|
+
# Dispatches a `keydown` event.
|
35
|
+
#
|
36
|
+
# @param [String, Symbol] key
|
37
|
+
# Name of the key, such as `"a"`, `:enter`, or `:backspace`.
|
38
|
+
#
|
39
|
+
# @return [self]
|
40
|
+
#
|
33
41
|
def down(key)
|
34
|
-
key = normalize_keys(Array(key))
|
42
|
+
key = normalize_keys(Array(key)).first
|
35
43
|
type = key[:text] ? "keyDown" : "rawKeyDown"
|
36
44
|
@page.command("Input.dispatchKeyEvent", slowmoable: true, type: type, **key)
|
37
45
|
self
|
38
46
|
end
|
39
47
|
|
48
|
+
#
|
49
|
+
# Dispatches a `keyup` event.
|
50
|
+
#
|
51
|
+
# @param [String, Symbol] key
|
52
|
+
# Name of the key, such as `"a"`, `:enter`, or `:backspace`.
|
53
|
+
#
|
54
|
+
# @return [self]
|
55
|
+
#
|
40
56
|
def up(key)
|
41
|
-
key = normalize_keys(Array(key))
|
57
|
+
key = normalize_keys(Array(key)).first
|
42
58
|
@page.command("Input.dispatchKeyEvent", slowmoable: true, type: "keyUp", **key)
|
43
59
|
self
|
44
60
|
end
|
45
61
|
|
62
|
+
#
|
63
|
+
# Sends a keydown, keypress/input, and keyup event for each character in
|
64
|
+
# the text.
|
65
|
+
#
|
66
|
+
# @param [Array<String, Symbol, (Symbol, String)>] keys
|
67
|
+
# The text to type into a focused element, `[:Shift, "s"], "tring"`.
|
68
|
+
#
|
69
|
+
# @return [self]
|
70
|
+
#
|
46
71
|
def type(*keys)
|
47
72
|
keys = normalize_keys(Array(keys))
|
48
73
|
|
@@ -55,15 +80,27 @@ module Ferrum
|
|
55
80
|
self
|
56
81
|
end
|
57
82
|
|
83
|
+
#
|
84
|
+
# Returns bitfield for a given keys.
|
85
|
+
#
|
86
|
+
# @param [Array<:alt, :ctrl, :command, :shift>] keys
|
87
|
+
#
|
88
|
+
# @return [Integer]
|
89
|
+
#
|
58
90
|
def modifiers(keys)
|
59
91
|
keys.map { |k| MODIFIERS[k.to_s] }.compact.reduce(0, :|)
|
60
92
|
end
|
61
93
|
|
62
94
|
private
|
63
95
|
|
96
|
+
# TODO: Refactor it, and try to simplify complexity
|
97
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
98
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
64
99
|
def normalize_keys(keys, pressed_keys = [], memo = [])
|
65
100
|
case keys
|
66
101
|
when Array
|
102
|
+
raise ArgumentError, "empty keys passed" if keys.empty?
|
103
|
+
|
67
104
|
pressed_keys.push([])
|
68
105
|
memo += combine_strings(keys).map do |key|
|
69
106
|
normalize_keys(key, pressed_keys, memo)
|
@@ -77,33 +114,39 @@ module Ferrum
|
|
77
114
|
pressed_keys.last.push(key)
|
78
115
|
nil
|
79
116
|
else
|
80
|
-
|
81
|
-
|
82
|
-
to_options(
|
117
|
+
key = KEYS.fetch(KEYS_MAPPING[key.to_sym] || key.to_sym)
|
118
|
+
key[:modifiers] = pressed_keys.flatten.map { |k| MODIFIERS[k] }.reduce(0, :|)
|
119
|
+
to_options(key)
|
83
120
|
end
|
84
121
|
when String
|
122
|
+
raise ArgumentError, "empty keys passed" if keys.empty?
|
123
|
+
|
85
124
|
pressed = pressed_keys.flatten
|
86
125
|
keys.each_char.map do |char|
|
126
|
+
key = KEYS[char] || {}
|
127
|
+
|
87
128
|
if pressed.empty?
|
88
|
-
key = KEYS[char] || {}
|
89
129
|
key = key.merge(text: char, unmodifiedText: char)
|
90
130
|
[to_options(key)]
|
91
131
|
else
|
92
|
-
key = KEYS[char] || {}
|
93
132
|
text = pressed == ["shift"] ? char.upcase : char
|
94
133
|
key = key.merge(
|
95
134
|
text: text,
|
96
135
|
unmodifiedText: text,
|
97
136
|
isKeypad: key["location"] == 3,
|
98
|
-
modifiers: pressed.map { |k| MODIFIERS[k] }.reduce(0, :|)
|
137
|
+
modifiers: pressed.map { |k| MODIFIERS[k] }.reduce(0, :|)
|
99
138
|
)
|
100
139
|
|
101
140
|
modifiers = pressed.map { |k| to_options(KEYS.fetch(KEYS_MAPPING[k.to_sym])) }
|
102
141
|
modifiers + [to_options(key)]
|
103
142
|
end.flatten
|
104
143
|
end
|
144
|
+
else
|
145
|
+
raise ArgumentError, "unexpected argument"
|
105
146
|
end
|
106
147
|
end
|
148
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
149
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
107
150
|
|
108
151
|
def combine_strings(keys)
|
109
152
|
keys
|
data/lib/ferrum/mouse.rb
CHANGED
@@ -10,10 +10,50 @@ module Ferrum
|
|
10
10
|
@x = @y = 0
|
11
11
|
end
|
12
12
|
|
13
|
+
#
|
14
|
+
# Scroll page to a given x, y coordinates.
|
15
|
+
#
|
16
|
+
# @param [Integer] top
|
17
|
+
# The pixel along the horizontal axis of the document that you want
|
18
|
+
# displayed in the upper left.
|
19
|
+
#
|
20
|
+
# @param [Integer] left
|
21
|
+
# The pixel along the vertical axis of the document that you want
|
22
|
+
# displayed in the upper left.
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# browser.go_to("https://www.google.com/search?q=Ruby+headless+driver+for+Capybara")
|
26
|
+
# browser.mouse.scroll_to(0, 400)
|
27
|
+
#
|
13
28
|
def scroll_to(top, left)
|
14
29
|
tap { @page.execute("window.scrollTo(#{top}, #{left})") }
|
15
30
|
end
|
16
31
|
|
32
|
+
#
|
33
|
+
# Click given coordinates, fires mouse move, down and up events.
|
34
|
+
#
|
35
|
+
# @param [Integer] x
|
36
|
+
#
|
37
|
+
# @param [Integer] y
|
38
|
+
#
|
39
|
+
# @param [Float] delay
|
40
|
+
# Delay between mouse down and mouse up events.
|
41
|
+
#
|
42
|
+
# @param [Float] wait
|
43
|
+
#
|
44
|
+
# @param [Hash{Symbol => Object}] options
|
45
|
+
# Additional keyword arguments.
|
46
|
+
#
|
47
|
+
# @option options [:left, :right] :button (:left)
|
48
|
+
# The mouse button to click.
|
49
|
+
#
|
50
|
+
# @option options [Integer] :count (1)
|
51
|
+
#
|
52
|
+
# @option options [Integer] :modifiers
|
53
|
+
# Bitfield for key modifiers. See`keyboard.modifiers`.
|
54
|
+
#
|
55
|
+
# @return [self]
|
56
|
+
#
|
17
57
|
def click(x:, y:, delay: 0, wait: CLICK_WAIT, **options)
|
18
58
|
move(x: x, y: y)
|
19
59
|
down(**options)
|
@@ -24,21 +64,67 @@ module Ferrum
|
|
24
64
|
self
|
25
65
|
end
|
26
66
|
|
67
|
+
#
|
68
|
+
# Mouse down for given coordinates.
|
69
|
+
#
|
70
|
+
# @param [Hash{Symbol => Object}] options
|
71
|
+
# Additional keyword arguments.
|
72
|
+
#
|
73
|
+
# @option options [:left, :right] :button (:left)
|
74
|
+
# The mouse button to click.
|
75
|
+
#
|
76
|
+
# @option options [Integer] :count (1)
|
77
|
+
#
|
78
|
+
# @option options [Integer] :modifiers
|
79
|
+
# Bitfield for key modifiers. See`keyboard.modifiers`.
|
80
|
+
#
|
81
|
+
# @return [self]
|
82
|
+
#
|
27
83
|
def down(**options)
|
28
84
|
tap { mouse_event(type: "mousePressed", **options) }
|
29
85
|
end
|
30
86
|
|
87
|
+
#
|
88
|
+
# Mouse up for given coordinates.
|
89
|
+
#
|
90
|
+
# @param [Hash{Symbol => Object}] options
|
91
|
+
# Additional keyword arguments.
|
92
|
+
#
|
93
|
+
# @option options [:left, :right] :button (:left)
|
94
|
+
# The mouse button to click.
|
95
|
+
#
|
96
|
+
# @option options [Integer] :count (1)
|
97
|
+
#
|
98
|
+
# @option options [Integer] :modifiers
|
99
|
+
# Bitfield for key modifiers. See`keyboard.modifiers`.
|
100
|
+
#
|
101
|
+
# @return [self]
|
102
|
+
#
|
31
103
|
def up(**options)
|
32
104
|
tap { mouse_event(type: "mouseReleased", **options) }
|
33
105
|
end
|
34
106
|
|
107
|
+
#
|
108
|
+
# Mouse move to given x and y.
|
109
|
+
#
|
110
|
+
# @param [Integer] x
|
111
|
+
#
|
112
|
+
# @param [Integer] y
|
113
|
+
#
|
114
|
+
# @param [Integer] steps
|
115
|
+
# Sends intermediate mousemove events.
|
116
|
+
#
|
117
|
+
# @return [self]
|
118
|
+
#
|
35
119
|
def move(x:, y:, steps: 1)
|
36
|
-
from_x
|
37
|
-
|
120
|
+
from_x = @x
|
121
|
+
from_y = @y
|
122
|
+
@x = x
|
123
|
+
@y = y
|
38
124
|
|
39
125
|
steps.times do |i|
|
40
|
-
new_x = from_x + (@x - from_x) * ((i + 1) / steps.to_f)
|
41
|
-
new_y = from_y + (@y - from_y) * ((i + 1) / steps.to_f)
|
126
|
+
new_x = from_x + ((@x - from_x) * ((i + 1) / steps.to_f))
|
127
|
+
new_y = from_y + ((@y - from_y) * ((i + 1) / steps.to_f))
|
42
128
|
|
43
129
|
@page.command("Input.dispatchMouseEvent",
|
44
130
|
slowmoable: true,
|
@@ -61,9 +147,8 @@ module Ferrum
|
|
61
147
|
|
62
148
|
def validate_button(button)
|
63
149
|
button = button.to_s
|
64
|
-
unless VALID_BUTTONS.include?(button)
|
65
|
-
|
66
|
-
end
|
150
|
+
raise "Invalid button: #{button}" unless VALID_BUTTONS.include?(button)
|
151
|
+
|
67
152
|
button
|
68
153
|
end
|
69
154
|
end
|
@@ -6,7 +6,8 @@ module Ferrum
|
|
6
6
|
attr_accessor :request_id, :frame_id, :resource_type
|
7
7
|
|
8
8
|
def initialize(page, params)
|
9
|
-
@page
|
9
|
+
@page = page
|
10
|
+
@params = params
|
10
11
|
@request_id = params["requestId"]
|
11
12
|
@frame_id = params["frameId"]
|
12
13
|
@resource_type = params["resourceType"]
|
@@ -55,7 +56,11 @@ module Ferrum
|
|
55
56
|
end
|
56
57
|
|
57
58
|
def inspect
|
58
|
-
|
59
|
+
"#<#{self.class} " \
|
60
|
+
"@request_id=#{@request_id.inspect} " \
|
61
|
+
"@frame_id=#{@frame_id.inspect} " \
|
62
|
+
"@resource_type=#{@resource_type.inspect} " \
|
63
|
+
"@request=#{@request.inspect}>"
|
59
64
|
end
|
60
65
|
end
|
61
66
|
end
|
@@ -3,48 +3,133 @@
|
|
3
3
|
module Ferrum
|
4
4
|
class Network
|
5
5
|
class Exchange
|
6
|
+
# ID of the request.
|
7
|
+
#
|
8
|
+
# @return String
|
6
9
|
attr_reader :id
|
10
|
+
|
11
|
+
# The intercepted request.
|
12
|
+
#
|
13
|
+
# @return [InterceptedRequest, nil]
|
7
14
|
attr_accessor :intercepted_request
|
8
|
-
attr_accessor :request, :response, :error
|
9
15
|
|
16
|
+
# The request object.
|
17
|
+
#
|
18
|
+
# @return [Request, nil]
|
19
|
+
attr_accessor :request
|
20
|
+
|
21
|
+
# The response object.
|
22
|
+
#
|
23
|
+
# @return [Response, nil]
|
24
|
+
attr_accessor :response
|
25
|
+
|
26
|
+
# The error object.
|
27
|
+
#
|
28
|
+
# @return [Error, nil]
|
29
|
+
attr_accessor :error
|
30
|
+
|
31
|
+
#
|
32
|
+
# Initializes the network exchange.
|
33
|
+
#
|
34
|
+
# @param [Page] page
|
35
|
+
#
|
36
|
+
# @param [String] id
|
37
|
+
#
|
10
38
|
def initialize(page, id)
|
11
|
-
@
|
39
|
+
@id = id
|
40
|
+
@page = page
|
12
41
|
@intercepted_request = nil
|
13
42
|
@request = @response = @error = nil
|
14
43
|
end
|
15
44
|
|
45
|
+
#
|
46
|
+
# Determines if the network exchange was caused by a page navigation
|
47
|
+
# event.
|
48
|
+
#
|
49
|
+
# @param [String] frame_id
|
50
|
+
#
|
51
|
+
# @return [Boolean]
|
52
|
+
#
|
16
53
|
def navigation_request?(frame_id)
|
17
|
-
request.type?(:document) &&
|
18
|
-
request.frame_id == frame_id
|
54
|
+
request.type?(:document) && request&.frame_id == frame_id
|
19
55
|
end
|
20
56
|
|
57
|
+
#
|
58
|
+
# Determines if the network exchange has a request.
|
59
|
+
#
|
60
|
+
# @return [Boolean]
|
61
|
+
#
|
21
62
|
def blank?
|
22
63
|
!request
|
23
64
|
end
|
24
65
|
|
66
|
+
#
|
67
|
+
# Determines if the request was intercepted and blocked.
|
68
|
+
#
|
69
|
+
# @return [Boolean]
|
70
|
+
#
|
25
71
|
def blocked?
|
26
|
-
|
72
|
+
intercepted? && intercepted_request.status?(:aborted)
|
27
73
|
end
|
28
74
|
|
75
|
+
#
|
76
|
+
# Determines if the request was blocked, a response was returned, or if an
|
77
|
+
# error occurred.
|
78
|
+
#
|
79
|
+
# @return [Boolean]
|
80
|
+
#
|
29
81
|
def finished?
|
30
|
-
blocked? || response || error
|
82
|
+
blocked? || !response.nil? || !error.nil?
|
31
83
|
end
|
32
84
|
|
85
|
+
#
|
86
|
+
# Determines if the network exchange is still not finished.
|
87
|
+
#
|
88
|
+
# @return [Boolean]
|
89
|
+
#
|
33
90
|
def pending?
|
34
91
|
!finished?
|
35
92
|
end
|
36
93
|
|
94
|
+
#
|
95
|
+
# Determines if the exchange's request was intercepted.
|
96
|
+
#
|
97
|
+
# @return [Boolean]
|
98
|
+
#
|
99
|
+
def intercepted?
|
100
|
+
!intercepted_request.nil?
|
101
|
+
end
|
102
|
+
|
103
|
+
#
|
104
|
+
# Returns request's URL.
|
105
|
+
#
|
106
|
+
# @return [String, nil]
|
107
|
+
#
|
108
|
+
def url
|
109
|
+
request&.url
|
110
|
+
end
|
111
|
+
|
112
|
+
#
|
113
|
+
# Converts the network exchange into a request, response, and error tuple.
|
114
|
+
#
|
115
|
+
# @return [Array]
|
116
|
+
#
|
37
117
|
def to_a
|
38
118
|
[request, response, error]
|
39
119
|
end
|
40
120
|
|
121
|
+
#
|
122
|
+
# Inspects the network exchange.
|
123
|
+
#
|
124
|
+
# @return [String]
|
125
|
+
#
|
41
126
|
def inspect
|
42
|
-
"#<#{self.class} "\
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
127
|
+
"#<#{self.class} " \
|
128
|
+
"@id=#{@id.inspect} " \
|
129
|
+
"@intercepted_request=#{@intercepted_request.inspect} " \
|
130
|
+
"@request=#{@request.inspect} " \
|
131
|
+
"@response=#{@response.inspect} " \
|
132
|
+
"@error=#{@error.inspect}>"
|
48
133
|
end
|
49
134
|
end
|
50
135
|
end
|