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