cuprite 0.14.1 → 0.14.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Cuprite
5
+ VERSION = "0.14.2"
6
+ end
7
+ 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.1
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