cuprite 0.2.0
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 +7 -0
- data/lib/capybara/cuprite.rb +28 -0
- data/lib/capybara/cuprite/browser.rb +286 -0
- data/lib/capybara/cuprite/browser/client.rb +70 -0
- data/lib/capybara/cuprite/browser/dom.rb +50 -0
- data/lib/capybara/cuprite/browser/frame.rb +109 -0
- data/lib/capybara/cuprite/browser/input.rb +123 -0
- data/lib/capybara/cuprite/browser/javascripts/index.js +407 -0
- data/lib/capybara/cuprite/browser/page.rb +278 -0
- data/lib/capybara/cuprite/browser/process.rb +167 -0
- data/lib/capybara/cuprite/browser/runtime.rb +194 -0
- data/lib/capybara/cuprite/browser/targets.rb +109 -0
- data/lib/capybara/cuprite/browser/web_socket.rb +60 -0
- data/lib/capybara/cuprite/cookie.rb +47 -0
- data/lib/capybara/cuprite/driver.rb +396 -0
- data/lib/capybara/cuprite/errors.rb +131 -0
- data/lib/capybara/cuprite/network/error.rb +25 -0
- data/lib/capybara/cuprite/network/request.rb +33 -0
- data/lib/capybara/cuprite/network/response.rb +42 -0
- data/lib/capybara/cuprite/node.rb +216 -0
- data/lib/capybara/cuprite/version.rb +7 -0
- metadata +231 -0
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Capybara
|
4
|
+
module Cuprite
|
5
|
+
class Error < StandardError; end
|
6
|
+
class NoSuchWindowError < Error; end
|
7
|
+
|
8
|
+
class ClientError < Error
|
9
|
+
attr_reader :response
|
10
|
+
|
11
|
+
def initialize(response)
|
12
|
+
@response = response
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class BrowserError < ClientError
|
17
|
+
def code
|
18
|
+
response["code"]
|
19
|
+
end
|
20
|
+
|
21
|
+
def data
|
22
|
+
response["data"]
|
23
|
+
end
|
24
|
+
|
25
|
+
def message
|
26
|
+
response["message"]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class JavaScriptError < ClientError
|
31
|
+
attr_reader :class_name, :message
|
32
|
+
|
33
|
+
def initialize(response)
|
34
|
+
super
|
35
|
+
@class_name, @message = response.values_at("className", "description")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class StatusFailError < ClientError
|
40
|
+
def message
|
41
|
+
"Request to #{response["url"]} failed to reach server, check DNS and/or server status"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class FrameNotFound < ClientError
|
46
|
+
def name
|
47
|
+
response["args"].first
|
48
|
+
end
|
49
|
+
|
50
|
+
def message
|
51
|
+
"The frame "#{name}" was not found."
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class InvalidSelector < ClientError
|
56
|
+
def initialize(response, method, selector)
|
57
|
+
super(response)
|
58
|
+
@method, @selector = method, selector
|
59
|
+
end
|
60
|
+
|
61
|
+
def message
|
62
|
+
"Browser raised error trying to find #{@method}: #{@selector.inspect}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class MouseEventFailed < ClientError
|
67
|
+
attr_reader :name, :selector, :position
|
68
|
+
|
69
|
+
def initialize(*)
|
70
|
+
super
|
71
|
+
data = /\A\w+: (\w+), (.+?), ([\d\.-]+), ([\d\.-]+)/.match(@response)
|
72
|
+
@name, @selector = data.values_at(1, 2)
|
73
|
+
@position = data.values_at(3, 4).map(&:to_f)
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
def message
|
78
|
+
"Firing a #{name} at coordinates [#{position.join(", ")}] failed. Cuprite detected " \
|
79
|
+
"another element with CSS selector \"#{selector}\" at this position. " \
|
80
|
+
"It may be overlapping the element you are trying to interact with. " \
|
81
|
+
"If you don't care about overlapping elements, try using node.trigger(\"#{name}\")."
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class NodeError < ClientError
|
86
|
+
attr_reader :node
|
87
|
+
|
88
|
+
def initialize(node, response)
|
89
|
+
@node = node
|
90
|
+
super(response)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class ObsoleteNode < NodeError
|
95
|
+
def message
|
96
|
+
"The element you are trying to interact with is either not part of the DOM, or is " \
|
97
|
+
"not currently visible on the page (perhaps display: none is set). " \
|
98
|
+
"It is possible the element has been replaced by another element and you meant to interact with " \
|
99
|
+
"the new element. If so you need to do a new find in order to get a reference to the " \
|
100
|
+
"new element."
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
class KeyError < ::ArgumentError
|
105
|
+
def initialize(response)
|
106
|
+
super(response["args"].first)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
class TimeoutError < Error
|
111
|
+
def message
|
112
|
+
"Timed out waiting for response. It's possible that this happened " \
|
113
|
+
"because something took a very long time (for example a page load " \
|
114
|
+
"was slow). If so, setting the Cuprite :timeout option to a higher " \
|
115
|
+
"value might help."
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
class ScriptTimeoutError < Error
|
120
|
+
def message
|
121
|
+
"Timed out waiting for evaluated script to resturn a value"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class DeadBrowser < Error
|
126
|
+
def initialize(message = "Chrome is dead")
|
127
|
+
super
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Capybara::Cuprite::Network
|
4
|
+
class Error
|
5
|
+
def initialize(data)
|
6
|
+
@data = data
|
7
|
+
end
|
8
|
+
|
9
|
+
def id
|
10
|
+
@data["networkRequestId"]
|
11
|
+
end
|
12
|
+
|
13
|
+
def url
|
14
|
+
@data["url"]
|
15
|
+
end
|
16
|
+
|
17
|
+
def description
|
18
|
+
@data["text"]
|
19
|
+
end
|
20
|
+
|
21
|
+
def time
|
22
|
+
@time ||= Time.strptime(@data["timestamp"].to_s, "%s")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "time"
|
4
|
+
|
5
|
+
module Capybara::Cuprite::Network
|
6
|
+
class Request
|
7
|
+
attr_accessor :response, :error
|
8
|
+
|
9
|
+
def initialize(data)
|
10
|
+
@data = data
|
11
|
+
end
|
12
|
+
|
13
|
+
def id
|
14
|
+
@data["id"]
|
15
|
+
end
|
16
|
+
|
17
|
+
def url
|
18
|
+
@data["url"]
|
19
|
+
end
|
20
|
+
|
21
|
+
def method
|
22
|
+
@data["method"]
|
23
|
+
end
|
24
|
+
|
25
|
+
def headers
|
26
|
+
@data["headers"]
|
27
|
+
end
|
28
|
+
|
29
|
+
def time
|
30
|
+
@time ||= Time.strptime(@data["time"].to_s, "%s")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Capybara::Cuprite::Network
|
4
|
+
class Response
|
5
|
+
def initialize(data)
|
6
|
+
@data = data
|
7
|
+
end
|
8
|
+
|
9
|
+
def id
|
10
|
+
@data["id"]
|
11
|
+
end
|
12
|
+
|
13
|
+
def url
|
14
|
+
@data["url"]
|
15
|
+
end
|
16
|
+
|
17
|
+
def status
|
18
|
+
@data["status"]
|
19
|
+
end
|
20
|
+
|
21
|
+
def status_text
|
22
|
+
@data["statusText"]
|
23
|
+
end
|
24
|
+
|
25
|
+
def headers
|
26
|
+
@data["headers"]
|
27
|
+
end
|
28
|
+
|
29
|
+
# FIXME: didn't check if we have it on redirect response
|
30
|
+
def redirect_url
|
31
|
+
@data["redirectURL"]
|
32
|
+
end
|
33
|
+
|
34
|
+
def body_size
|
35
|
+
@body_size ||= @data.dig("headers", "Content-Length").to_i
|
36
|
+
end
|
37
|
+
|
38
|
+
def content_type
|
39
|
+
@content_type ||= @data.dig("headers", "contentType").sub(/;.*\z/, "")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,216 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Capybara::Cuprite
|
4
|
+
class Node < Capybara::Driver::Node
|
5
|
+
attr_reader :target_id, :node
|
6
|
+
|
7
|
+
def initialize(driver, target_id, node)
|
8
|
+
super(driver, self)
|
9
|
+
@target_id, @node = target_id, node
|
10
|
+
end
|
11
|
+
|
12
|
+
def browser
|
13
|
+
driver.browser
|
14
|
+
end
|
15
|
+
|
16
|
+
def command(name, *args)
|
17
|
+
browser.send(name, @node, *args)
|
18
|
+
rescue BrowserError => e
|
19
|
+
case e.message
|
20
|
+
when "Cuprite.ObsoleteNode"
|
21
|
+
raise ObsoleteNode.new(self, e.response)
|
22
|
+
when "Cuprite.MouseEventFailed"
|
23
|
+
raise MouseEventFailed.new(self, e.response)
|
24
|
+
else
|
25
|
+
raise
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def parents
|
30
|
+
command(:parents).map do |parent|
|
31
|
+
self.class.new(driver, parent["target_id"], parent["node"])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def find(method, selector)
|
36
|
+
command(:find_within, method, selector).map do |node|
|
37
|
+
self.class.new(driver, @target_id, node)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def find_xpath(selector)
|
42
|
+
find(:xpath, selector)
|
43
|
+
end
|
44
|
+
|
45
|
+
def find_css(selector)
|
46
|
+
find(:css, selector)
|
47
|
+
end
|
48
|
+
|
49
|
+
def all_text
|
50
|
+
filter_text(command(:all_text))
|
51
|
+
end
|
52
|
+
|
53
|
+
def visible_text
|
54
|
+
if Capybara::VERSION.to_f < 3.0
|
55
|
+
filter_text(command(:visible_text))
|
56
|
+
else
|
57
|
+
command(:visible_text).to_s
|
58
|
+
.gsub(/\A[[:space:]&&[^\u00a0]]+/, "")
|
59
|
+
.gsub(/[[:space:]&&[^\u00a0]]+\z/, "")
|
60
|
+
.gsub(/\n+/, "\n")
|
61
|
+
.tr("\u00a0", " ")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def property(name)
|
66
|
+
command(:property, name)
|
67
|
+
end
|
68
|
+
|
69
|
+
def [](name)
|
70
|
+
# Although the attribute matters, the property is consistent. Return that in
|
71
|
+
# preference to the attribute for links and images.
|
72
|
+
if ((tag_name == "img") && (name == "src")) || ((tag_name == "a") && (name == "href"))
|
73
|
+
# if attribute exists get the property
|
74
|
+
return command(:attribute, name) && command(:property, name)
|
75
|
+
end
|
76
|
+
|
77
|
+
value = property(name)
|
78
|
+
value = command(:attribute, name) if value.nil? || value.is_a?(Hash)
|
79
|
+
|
80
|
+
value
|
81
|
+
end
|
82
|
+
|
83
|
+
def attributes
|
84
|
+
command(:attributes)
|
85
|
+
end
|
86
|
+
|
87
|
+
def value
|
88
|
+
command(:value)
|
89
|
+
end
|
90
|
+
|
91
|
+
def set(value, options = {})
|
92
|
+
warn "Options passed to Node#set but Cuprite doesn't currently support any - ignoring" unless options.empty?
|
93
|
+
|
94
|
+
if tag_name == "input"
|
95
|
+
case self[:type]
|
96
|
+
when "radio"
|
97
|
+
click
|
98
|
+
when "checkbox"
|
99
|
+
click if value != checked?
|
100
|
+
when "file"
|
101
|
+
files = value.respond_to?(:to_ary) ? value.to_ary.map(&:to_s) : value.to_s
|
102
|
+
command(:select_file, files)
|
103
|
+
else
|
104
|
+
command(:set, value.to_s)
|
105
|
+
end
|
106
|
+
elsif tag_name == "textarea"
|
107
|
+
command(:set, value.to_s)
|
108
|
+
elsif self[:isContentEditable]
|
109
|
+
command(:delete_text)
|
110
|
+
send_keys(value.to_s)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def select_option
|
115
|
+
command(:select, true)
|
116
|
+
end
|
117
|
+
|
118
|
+
def unselect_option
|
119
|
+
command(:select, false) ||
|
120
|
+
raise(Capybara::UnselectNotAllowed, "Cannot unselect option from single select box.")
|
121
|
+
end
|
122
|
+
|
123
|
+
def tag_name
|
124
|
+
@tag_name ||= @node["nodeName"].downcase
|
125
|
+
end
|
126
|
+
|
127
|
+
def visible?
|
128
|
+
command(:visible?)
|
129
|
+
end
|
130
|
+
|
131
|
+
def checked?
|
132
|
+
self[:checked]
|
133
|
+
end
|
134
|
+
|
135
|
+
def selected?
|
136
|
+
!!self[:selected]
|
137
|
+
end
|
138
|
+
|
139
|
+
def disabled?
|
140
|
+
command(:disabled?)
|
141
|
+
end
|
142
|
+
|
143
|
+
def click(keys = [], offset = {})
|
144
|
+
command(:click, keys, offset)
|
145
|
+
end
|
146
|
+
|
147
|
+
def right_click(keys = [], offset = {})
|
148
|
+
command(:right_click, keys, offset)
|
149
|
+
end
|
150
|
+
|
151
|
+
def double_click(keys = [], offset = {})
|
152
|
+
command(:double_click, keys, offset)
|
153
|
+
end
|
154
|
+
|
155
|
+
def hover
|
156
|
+
command(:hover)
|
157
|
+
end
|
158
|
+
|
159
|
+
def drag_to(other)
|
160
|
+
command(:drag, other.node)
|
161
|
+
end
|
162
|
+
|
163
|
+
def drag_by(x, y)
|
164
|
+
command(:drag_by, x, y)
|
165
|
+
end
|
166
|
+
|
167
|
+
def trigger(event)
|
168
|
+
command(:trigger, event)
|
169
|
+
end
|
170
|
+
|
171
|
+
def ==(other)
|
172
|
+
# We compare backendNodeId because once nodeId is sent to frontend backend
|
173
|
+
# never returns same nodeId sending 0. In other words frontend is
|
174
|
+
# responsible for keeping track of node ids.
|
175
|
+
@target_id == other.target_id && @node["backendNodeId"] == other.node["backendNodeId"]
|
176
|
+
end
|
177
|
+
|
178
|
+
def send_keys(*keys)
|
179
|
+
command(:send_keys, keys)
|
180
|
+
end
|
181
|
+
alias_method :send_key, :send_keys
|
182
|
+
|
183
|
+
def path
|
184
|
+
command(:path)
|
185
|
+
end
|
186
|
+
|
187
|
+
def inspect
|
188
|
+
%(#<#{self.class} @target_id=#{@target_id.inspect} @node=#{@node.inspect}>)
|
189
|
+
end
|
190
|
+
|
191
|
+
# @api private
|
192
|
+
def to_json(*)
|
193
|
+
JSON.generate(as_json)
|
194
|
+
end
|
195
|
+
|
196
|
+
# @api private
|
197
|
+
def as_json(*)
|
198
|
+
# FIXME: Where this method is used and why attr is called id?
|
199
|
+
{ ELEMENT: { target_id: @target_id, id: @node } }
|
200
|
+
end
|
201
|
+
|
202
|
+
private
|
203
|
+
|
204
|
+
def filter_text(text)
|
205
|
+
if Capybara::VERSION.to_f < 3
|
206
|
+
Capybara::Helpers.normalize_whitespace(text.to_s)
|
207
|
+
else
|
208
|
+
text.gsub(/[\u200b\u200e\u200f]/, "")
|
209
|
+
.gsub(/[\ \n\f\t\v\u2028\u2029]+/, " ")
|
210
|
+
.gsub(/\A[[:space:]&&[^\u00a0]]+/, "")
|
211
|
+
.gsub(/[[:space:]&&[^\u00a0]]+\z/, "")
|
212
|
+
.tr("\u00a0", " ")
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|