cuprite 0.14.1 → 0.14.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/LICENSE +21 -0
- data/README.md +212 -0
- data/lib/capybara/cuprite/browser.rb +219 -0
- data/lib/capybara/cuprite/cookie.rb +47 -0
- data/lib/capybara/cuprite/driver.rb +424 -0
- data/lib/capybara/cuprite/errors.rb +63 -0
- data/lib/capybara/cuprite/javascripts/index.js +478 -0
- data/lib/capybara/cuprite/node.rb +285 -0
- data/lib/capybara/cuprite/page.rb +205 -0
- data/lib/capybara/cuprite/version.rb +7 -0
- data/lib/capybara/cuprite.rb +13 -0
- metadata +13 -2
@@ -0,0 +1,285 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module Capybara
|
6
|
+
module Cuprite
|
7
|
+
class Node < Capybara::Driver::Node
|
8
|
+
attr_reader :node
|
9
|
+
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
delegate %i[description] => :node
|
13
|
+
delegate %i[browser] => :driver
|
14
|
+
|
15
|
+
def initialize(driver, node)
|
16
|
+
super(driver, self)
|
17
|
+
@node = node
|
18
|
+
end
|
19
|
+
|
20
|
+
def command(name, *args)
|
21
|
+
browser.send(name, node, *args)
|
22
|
+
rescue Ferrum::NodeNotFoundError => e
|
23
|
+
raise ObsoleteNode.new(self, e.response)
|
24
|
+
rescue Ferrum::BrowserError => e
|
25
|
+
case e.message
|
26
|
+
when "Cuprite.MouseEventFailed"
|
27
|
+
raise MouseEventFailed.new(self, e.response)
|
28
|
+
else
|
29
|
+
raise
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def parents
|
34
|
+
command(:parents).map do |parent|
|
35
|
+
self.class.new(driver, parent)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def find_xpath(selector)
|
40
|
+
find(:xpath, selector)
|
41
|
+
end
|
42
|
+
|
43
|
+
def find_css(selector)
|
44
|
+
find(:css, selector)
|
45
|
+
end
|
46
|
+
|
47
|
+
def find(method, selector)
|
48
|
+
command(:find_within, method, selector).map do |node|
|
49
|
+
self.class.new(driver, node)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def all_text
|
54
|
+
filter_text(command(:all_text))
|
55
|
+
end
|
56
|
+
|
57
|
+
def visible_text
|
58
|
+
command(:visible_text).to_s
|
59
|
+
.gsub(/\A[[:space:]&&[^\u00a0]]+/, "")
|
60
|
+
.gsub(/[[:space:]&&[^\u00a0]]+\z/, "")
|
61
|
+
.gsub(/\n+/, "\n")
|
62
|
+
.tr("\u00a0", " ")
|
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") ||
|
73
|
+
(tag_name == "a" && name == "href")
|
74
|
+
# if attribute exists get the property
|
75
|
+
return command(:attribute, name) && command(:property, name)
|
76
|
+
end
|
77
|
+
|
78
|
+
value = property(name)
|
79
|
+
value = command(:attribute, name) if value.nil? || value.is_a?(Hash)
|
80
|
+
|
81
|
+
value
|
82
|
+
end
|
83
|
+
|
84
|
+
def attributes
|
85
|
+
command(:attributes)
|
86
|
+
end
|
87
|
+
|
88
|
+
def value
|
89
|
+
command(:value)
|
90
|
+
end
|
91
|
+
|
92
|
+
def set(value, options = {})
|
93
|
+
warn "Options passed to Node#set but Cuprite doesn't currently support any - ignoring" unless options.empty?
|
94
|
+
|
95
|
+
if tag_name == "input"
|
96
|
+
case self[:type]
|
97
|
+
when "radio"
|
98
|
+
click
|
99
|
+
when "checkbox"
|
100
|
+
click if value != checked?
|
101
|
+
when "file"
|
102
|
+
files = value.respond_to?(:to_ary) ? value.to_ary.map(&:to_s) : value.to_s
|
103
|
+
command(:select_file, files)
|
104
|
+
when "color"
|
105
|
+
node.evaluate("this.setAttribute('value', '#{value}')")
|
106
|
+
else
|
107
|
+
command(:set, value.to_s)
|
108
|
+
end
|
109
|
+
elsif tag_name == "textarea"
|
110
|
+
command(:set, value.to_s)
|
111
|
+
elsif self[:isContentEditable]
|
112
|
+
command(:delete_text)
|
113
|
+
send_keys(value.to_s)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def select_option
|
118
|
+
command(:select, true)
|
119
|
+
end
|
120
|
+
|
121
|
+
def unselect_option
|
122
|
+
command(:select, false) ||
|
123
|
+
raise(Capybara::UnselectNotAllowed, "Cannot unselect option from single select box.")
|
124
|
+
end
|
125
|
+
|
126
|
+
def tag_name
|
127
|
+
@tag_name ||= description["nodeName"].downcase
|
128
|
+
end
|
129
|
+
|
130
|
+
def visible?
|
131
|
+
command(:visible?)
|
132
|
+
end
|
133
|
+
|
134
|
+
def checked?
|
135
|
+
self[:checked]
|
136
|
+
end
|
137
|
+
|
138
|
+
def selected?
|
139
|
+
!!self[:selected]
|
140
|
+
end
|
141
|
+
|
142
|
+
def disabled?
|
143
|
+
command(:disabled?)
|
144
|
+
end
|
145
|
+
|
146
|
+
def click(keys = [], **options)
|
147
|
+
prepare_and_click(:left, __method__, keys, options)
|
148
|
+
end
|
149
|
+
|
150
|
+
def right_click(keys = [], **options)
|
151
|
+
prepare_and_click(:right, __method__, keys, options)
|
152
|
+
end
|
153
|
+
|
154
|
+
def double_click(keys = [], **options)
|
155
|
+
prepare_and_click(:double, __method__, keys, options)
|
156
|
+
end
|
157
|
+
|
158
|
+
def hover
|
159
|
+
command(:hover)
|
160
|
+
end
|
161
|
+
|
162
|
+
def drag_to(other)
|
163
|
+
command(:drag, other.node)
|
164
|
+
end
|
165
|
+
|
166
|
+
def drag_by(x, y)
|
167
|
+
command(:drag_by, x, y)
|
168
|
+
end
|
169
|
+
|
170
|
+
def trigger(event)
|
171
|
+
command(:trigger, event)
|
172
|
+
end
|
173
|
+
|
174
|
+
def scroll_to(element, location, position = nil)
|
175
|
+
if element.is_a?(Node)
|
176
|
+
scroll_element_to_location(element, location)
|
177
|
+
elsif location.is_a?(Symbol)
|
178
|
+
scroll_to_location(location)
|
179
|
+
else
|
180
|
+
scroll_to_coords(*position)
|
181
|
+
end
|
182
|
+
self
|
183
|
+
end
|
184
|
+
|
185
|
+
def scroll_by(x, y)
|
186
|
+
driver.execute_script <<~JS, self, x, y
|
187
|
+
var el = arguments[0];
|
188
|
+
if (el.scrollBy){
|
189
|
+
el.scrollBy(arguments[1], arguments[2]);
|
190
|
+
} else {
|
191
|
+
el.scrollTop = el.scrollTop + arguments[2];
|
192
|
+
el.scrollLeft = el.scrollLeft + arguments[1];
|
193
|
+
}
|
194
|
+
JS
|
195
|
+
end
|
196
|
+
|
197
|
+
def ==(other)
|
198
|
+
node == other.native.node
|
199
|
+
end
|
200
|
+
|
201
|
+
def send_keys(*keys)
|
202
|
+
command(:send_keys, keys)
|
203
|
+
end
|
204
|
+
alias send_key send_keys
|
205
|
+
|
206
|
+
def path
|
207
|
+
command(:path)
|
208
|
+
end
|
209
|
+
|
210
|
+
def inspect
|
211
|
+
%(#<#{self.class} @node=#{@node.inspect}>)
|
212
|
+
end
|
213
|
+
|
214
|
+
# @api private
|
215
|
+
def to_json(*)
|
216
|
+
JSON.generate(as_json)
|
217
|
+
end
|
218
|
+
|
219
|
+
# @api private
|
220
|
+
def as_json(*)
|
221
|
+
# FIXME: Where is this method used and why attr is called id?
|
222
|
+
{ ELEMENT: { node: node, id: node.node_id } }
|
223
|
+
end
|
224
|
+
|
225
|
+
private
|
226
|
+
|
227
|
+
def prepare_and_click(mode, name, keys, options)
|
228
|
+
delay = options[:delay].to_i
|
229
|
+
x, y = options.values_at(:x, :y)
|
230
|
+
offset = { x: x, y: y, position: options[:offset] || :top }
|
231
|
+
command(:before_click, name, keys, offset)
|
232
|
+
node.click(mode: mode, keys: keys, offset: offset, delay: delay)
|
233
|
+
end
|
234
|
+
|
235
|
+
def filter_text(text)
|
236
|
+
text.gsub(/[\u200b\u200e\u200f]/, "")
|
237
|
+
.gsub(/[\ \n\f\t\v\u2028\u2029]+/, " ")
|
238
|
+
.gsub(/\A[[:space:]&&[^\u00a0]]+/, "")
|
239
|
+
.gsub(/[[:space:]&&[^\u00a0]]+\z/, "")
|
240
|
+
.tr("\u00a0", " ")
|
241
|
+
end
|
242
|
+
|
243
|
+
def scroll_element_to_location(element, location)
|
244
|
+
scroll_opts = case location
|
245
|
+
when :top
|
246
|
+
"true"
|
247
|
+
when :bottom
|
248
|
+
"false"
|
249
|
+
when :center
|
250
|
+
"{behavior: 'instant', block: 'center'}"
|
251
|
+
else
|
252
|
+
raise ArgumentError, "Invalid scroll_to location: #{location}"
|
253
|
+
end
|
254
|
+
driver.execute_script <<~JS, element
|
255
|
+
arguments[0].scrollIntoView(#{scroll_opts})
|
256
|
+
JS
|
257
|
+
end
|
258
|
+
|
259
|
+
def scroll_to_location(location)
|
260
|
+
height = { top: "0",
|
261
|
+
bottom: "arguments[0].scrollHeight",
|
262
|
+
center: "(arguments[0].scrollHeight - arguments[0].clientHeight)/2" }
|
263
|
+
|
264
|
+
driver.execute_script <<~JS, self
|
265
|
+
if (arguments[0].scrollTo){
|
266
|
+
arguments[0].scrollTo(0, #{height[location]});
|
267
|
+
} else {
|
268
|
+
arguments[0].scrollTop = #{height[location]};
|
269
|
+
}
|
270
|
+
JS
|
271
|
+
end
|
272
|
+
|
273
|
+
def scroll_to_coords(x, y)
|
274
|
+
driver.execute_script <<~JS, self, x, y
|
275
|
+
if (arguments[0].scrollTo){
|
276
|
+
arguments[0].scrollTo(arguments[1], arguments[2]);
|
277
|
+
} else {
|
278
|
+
arguments[0].scrollTop = arguments[2];
|
279
|
+
arguments[0].scrollLeft = arguments[1];
|
280
|
+
}
|
281
|
+
JS
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module Capybara
|
6
|
+
module Cuprite
|
7
|
+
class Page < Ferrum::Page
|
8
|
+
MODAL_WAIT = ENV.fetch("CUPRITE_MODAL_WAIT", 0.05).to_f
|
9
|
+
TRIGGER_CLICK_WAIT = ENV.fetch("CUPRITE_TRIGGER_CLICK_WAIT", 0.1).to_f
|
10
|
+
|
11
|
+
extend Forwardable
|
12
|
+
delegate %i[at_css at_xpath css xpath
|
13
|
+
current_url current_title body execution_id
|
14
|
+
evaluate evaluate_on evaluate_async execute] => :active_frame
|
15
|
+
|
16
|
+
def initialize(*args)
|
17
|
+
@frame_stack = []
|
18
|
+
@accept_modal = []
|
19
|
+
@modal_messages = []
|
20
|
+
super
|
21
|
+
end
|
22
|
+
|
23
|
+
def set(node, value)
|
24
|
+
object_id = command("DOM.resolveNode", nodeId: node.node_id).dig("object", "objectId")
|
25
|
+
evaluate("_cuprite.set(arguments[0], arguments[1])", { "objectId" => object_id }, value)
|
26
|
+
end
|
27
|
+
|
28
|
+
def select(node, value)
|
29
|
+
evaluate_on(node: node, expression: "_cuprite.select(this, #{value})")
|
30
|
+
end
|
31
|
+
|
32
|
+
def trigger(node, event)
|
33
|
+
options = {}
|
34
|
+
options.merge!(wait: TRIGGER_CLICK_WAIT) if event.to_s == "click" && TRIGGER_CLICK_WAIT.positive?
|
35
|
+
evaluate_on(node: node, expression: %(_cuprite.trigger(this, "#{event}")), **options)
|
36
|
+
end
|
37
|
+
|
38
|
+
def hover(node)
|
39
|
+
evaluate_on(node: node, expression: "_cuprite.scrollIntoViewport(this)")
|
40
|
+
x, y = find_position(node)
|
41
|
+
command("Input.dispatchMouseEvent", type: "mouseMoved", x: x, y: y)
|
42
|
+
end
|
43
|
+
|
44
|
+
def send_keys(node, keys)
|
45
|
+
unless evaluate_on(node: node, expression: %(_cuprite.containsSelection(this)))
|
46
|
+
before_click(node, "click")
|
47
|
+
node.click(mode: :left, keys: keys)
|
48
|
+
end
|
49
|
+
|
50
|
+
keyboard.type(keys)
|
51
|
+
end
|
52
|
+
|
53
|
+
def accept_confirm
|
54
|
+
@accept_modal << true
|
55
|
+
end
|
56
|
+
|
57
|
+
def dismiss_confirm
|
58
|
+
@accept_modal << false
|
59
|
+
end
|
60
|
+
|
61
|
+
def accept_prompt(modal_response)
|
62
|
+
@accept_modal << true
|
63
|
+
@modal_response = modal_response
|
64
|
+
end
|
65
|
+
|
66
|
+
def dismiss_prompt
|
67
|
+
@accept_modal << false
|
68
|
+
end
|
69
|
+
|
70
|
+
def find_modal(options)
|
71
|
+
start = Ferrum::Utils::ElapsedTime.monotonic_time
|
72
|
+
timeout = options.fetch(:wait, browser.timeout)
|
73
|
+
expect_text = options[:text]
|
74
|
+
expect_regexp = expect_text.is_a?(Regexp) ? expect_text : Regexp.escape(expect_text.to_s)
|
75
|
+
not_found_msg = "Unable to find modal dialog"
|
76
|
+
not_found_msg += " with #{expect_text}" if expect_text
|
77
|
+
|
78
|
+
begin
|
79
|
+
modal_text = @modal_messages.shift
|
80
|
+
raise Capybara::ModalNotFound if modal_text.nil? || (expect_text && !modal_text.match(expect_regexp))
|
81
|
+
rescue Capybara::ModalNotFound => e
|
82
|
+
raise e, not_found_msg if Ferrum::Utils::ElapsedTime.timeout?(start, timeout)
|
83
|
+
|
84
|
+
sleep(MODAL_WAIT)
|
85
|
+
retry
|
86
|
+
end
|
87
|
+
|
88
|
+
modal_text
|
89
|
+
end
|
90
|
+
|
91
|
+
def reset_modals
|
92
|
+
@accept_modal = []
|
93
|
+
@modal_response = nil
|
94
|
+
@modal_messages = []
|
95
|
+
end
|
96
|
+
|
97
|
+
def before_click(node, name, _keys = [], offset = {})
|
98
|
+
evaluate_on(node: node, expression: "_cuprite.scrollIntoViewport(this)")
|
99
|
+
|
100
|
+
# If offset is given it may go outside of the element and likely error
|
101
|
+
# will be raised that we detected another element at this position.
|
102
|
+
return true if offset[:x] || offset[:y]
|
103
|
+
|
104
|
+
x, y = find_position(node, **offset)
|
105
|
+
evaluate_on(node: node, expression: "_cuprite.mouseEventTest(this, '#{name}', #{x}, #{y})")
|
106
|
+
true
|
107
|
+
rescue Ferrum::JavaScriptError => e
|
108
|
+
raise MouseEventFailed, e.message if e.class_name == "MouseEventFailed"
|
109
|
+
end
|
110
|
+
|
111
|
+
def switch_to_frame(handle)
|
112
|
+
case handle
|
113
|
+
when :parent
|
114
|
+
@frame_stack.pop
|
115
|
+
when :top
|
116
|
+
@frame_stack = []
|
117
|
+
else
|
118
|
+
@frame_stack << handle
|
119
|
+
inject_extensions
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def frame_name
|
124
|
+
evaluate("window.name")
|
125
|
+
end
|
126
|
+
|
127
|
+
def title
|
128
|
+
active_frame.current_title
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
134
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
135
|
+
# rubocop:disable Style/GuardClause
|
136
|
+
def prepare_page
|
137
|
+
super
|
138
|
+
|
139
|
+
network.intercept if !Array(@browser.url_whitelist).empty? ||
|
140
|
+
!Array(@browser.url_blacklist).empty?
|
141
|
+
|
142
|
+
on(:request) do |request, index, total|
|
143
|
+
if @browser.url_blacklist && !@browser.url_blacklist.empty?
|
144
|
+
if @browser.url_blacklist.any? { |r| request.match?(r) }
|
145
|
+
request.abort and next
|
146
|
+
else
|
147
|
+
request.continue and next
|
148
|
+
end
|
149
|
+
elsif @browser.url_whitelist && !@browser.url_whitelist.empty?
|
150
|
+
if @browser.url_whitelist.any? { |r| request.match?(r) }
|
151
|
+
request.continue and next
|
152
|
+
else
|
153
|
+
request.abort and next
|
154
|
+
end
|
155
|
+
elsif index + 1 < total
|
156
|
+
# There are other callbacks that may handle this request
|
157
|
+
next
|
158
|
+
else
|
159
|
+
# If there are no callbacks then just continue
|
160
|
+
request.continue
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
on("Page.javascriptDialogOpening") do |params|
|
165
|
+
accept_modal = @accept_modal.last
|
166
|
+
if [true, false].include?(accept_modal)
|
167
|
+
@accept_modal.pop
|
168
|
+
@modal_messages << params["message"]
|
169
|
+
options = { accept: accept_modal }
|
170
|
+
response = @modal_response || params["defaultPrompt"]
|
171
|
+
else
|
172
|
+
with_text = params["message"] ? "with text `#{params['message']}` " : ""
|
173
|
+
warn "Modal window #{with_text}has been opened, but you didn't wrap " \
|
174
|
+
"your code into (`accept_prompt` | `dismiss_prompt` | " \
|
175
|
+
"`accept_confirm` | `dismiss_confirm` | `accept_alert`), " \
|
176
|
+
"accepting by default"
|
177
|
+
options = { accept: true }
|
178
|
+
response = params["defaultPrompt"]
|
179
|
+
end
|
180
|
+
options.merge!(promptText: response) if response
|
181
|
+
command("Page.handleJavaScriptDialog", **options)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
185
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
186
|
+
# rubocop:enable Style/GuardClause
|
187
|
+
|
188
|
+
def find_position(node, **options)
|
189
|
+
node.find_position(**options)
|
190
|
+
rescue Ferrum::BrowserError => e
|
191
|
+
raise MouseEventFailed, "MouseEventFailed: click, none, 0, 0" if e.message == "Could not compute content quads."
|
192
|
+
|
193
|
+
raise
|
194
|
+
end
|
195
|
+
|
196
|
+
def active_frame
|
197
|
+
if @frame_stack.empty?
|
198
|
+
main_frame
|
199
|
+
else
|
200
|
+
@frames[@frame_stack.last]
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ferrum"
|
4
|
+
require "capybara"
|
5
|
+
require "capybara/cuprite/driver"
|
6
|
+
require "capybara/cuprite/browser"
|
7
|
+
require "capybara/cuprite/page"
|
8
|
+
require "capybara/cuprite/node"
|
9
|
+
require "capybara/cuprite/errors"
|
10
|
+
|
11
|
+
Capybara.register_driver(:cuprite) do |app|
|
12
|
+
Capybara::Cuprite::Driver.new(app)
|
13
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cuprite
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.14.
|
4
|
+
version: 0.14.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dmitry Vorotilin
|
@@ -171,7 +171,18 @@ email:
|
|
171
171
|
executables: []
|
172
172
|
extensions: []
|
173
173
|
extra_rdoc_files: []
|
174
|
-
files:
|
174
|
+
files:
|
175
|
+
- LICENSE
|
176
|
+
- README.md
|
177
|
+
- lib/capybara/cuprite.rb
|
178
|
+
- lib/capybara/cuprite/browser.rb
|
179
|
+
- lib/capybara/cuprite/cookie.rb
|
180
|
+
- lib/capybara/cuprite/driver.rb
|
181
|
+
- lib/capybara/cuprite/errors.rb
|
182
|
+
- lib/capybara/cuprite/javascripts/index.js
|
183
|
+
- lib/capybara/cuprite/node.rb
|
184
|
+
- lib/capybara/cuprite/page.rb
|
185
|
+
- lib/capybara/cuprite/version.rb
|
175
186
|
homepage: https://github.com/rubycdp/cuprite
|
176
187
|
licenses:
|
177
188
|
- MIT
|