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.
@@ -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