cuprite 0.6.0 → 0.7.1

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.
@@ -1,21 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "uri"
4
-
5
4
  require "forwardable"
6
5
 
7
6
  module Capybara::Cuprite
8
7
  class Driver < Capybara::Driver::Base
8
+ DEFAULT_MAXIMIZE_SCREEN_SIZE = [1366, 768].freeze
9
+ EXTENSION = File.expand_path("javascripts/index.js", __dir__)
10
+
9
11
  extend Forwardable
10
12
 
11
13
  delegate %i(restart quit status_code timeout timeout=) => :browser
12
14
 
13
- attr_reader :app, :options
15
+ attr_reader :app, :options, :screen_size
14
16
 
15
17
  def initialize(app, options = {})
16
18
  @app = app
17
- @options = options
19
+ @options = options.dup
18
20
  @started = false
21
+
22
+ @options[:extensions] ||= []
23
+ @options[:extensions] << EXTENSION
24
+
25
+ @screen_size = @options.delete(:screen_size)
26
+ @screen_size ||= DEFAULT_MAXIMIZE_SCREEN_SIZE
27
+
28
+ @options[:save_path] = Capybara.save_path.to_s if Capybara.save_path
19
29
  end
20
30
 
21
31
  def needs_server?
@@ -23,7 +33,7 @@ module Capybara::Cuprite
23
33
  end
24
34
 
25
35
  def browser
26
- @browser ||= Browser.start(@options)
36
+ @browser ||= Browser.new(@options)
27
37
  end
28
38
 
29
39
  def visit(url)
@@ -64,20 +74,20 @@ module Capybara::Cuprite
64
74
  browser.frame_title
65
75
  end
66
76
 
67
- def find(method, selector)
68
- browser.find(method, selector).map { |target_id, node| Node.new(self, target_id, node) }
69
- end
70
-
71
77
  def find_xpath(selector)
72
- find :xpath, selector
78
+ find(:xpath, selector)
73
79
  end
74
80
 
75
81
  def find_css(selector)
76
- find :css, selector
82
+ find(:css, selector)
83
+ end
84
+
85
+ def find(method, selector)
86
+ browser.find(method, selector).map { |native| Node.new(self, native) }
77
87
  end
78
88
 
79
89
  def click(x, y)
80
- browser.click_coordinates(x, y)
90
+ browser.mouse.click(x: x, y: y)
81
91
  end
82
92
 
83
93
  def evaluate_script(script, *args)
@@ -96,7 +106,14 @@ module Capybara::Cuprite
96
106
  end
97
107
 
98
108
  def switch_to_frame(locator)
99
- browser.switch_to_frame(locator)
109
+ handle = case locator
110
+ when Capybara::Node::Element
111
+ locator.native.description["frameId"]
112
+ when :parent, :top
113
+ locator
114
+ end
115
+
116
+ browser.switch_to_frame(handle)
100
117
  end
101
118
 
102
119
  def current_window_handle
@@ -124,31 +141,47 @@ module Capybara::Cuprite
124
141
  end
125
142
 
126
143
  def no_such_window_error
127
- NoSuchWindowError
144
+ Ferrum::NoSuchWindowError
128
145
  end
129
146
 
130
147
  def reset!
131
- browser.reset
148
+ @zoom_factor = nil
149
+ @paper_size = nil
132
150
  browser.url_blacklist = @options[:url_blacklist]
133
151
  browser.url_whitelist = @options[:url_whitelist]
152
+ browser.reset
134
153
  @started = false
135
154
  end
136
155
 
137
156
  def save_screenshot(path, options = {})
138
- browser.render(path, options)
157
+ options[:scale] = @zoom_factor if @zoom_factor
158
+
159
+ if pdf?(path, options)
160
+ options[:paperWidth] = @paper_size[:width].to_f if @paper_size
161
+ options[:paperHeight] = @paper_size[:height].to_f if @paper_size
162
+ browser.pdf(path: path, **options)
163
+ else
164
+ browser.screenshot(path: path, **options)
165
+ end
139
166
  end
140
167
  alias_method :render, :save_screenshot
141
168
 
142
169
  def render_base64(format = :png, options = {})
143
- browser.render_base64(format, options)
170
+ if pdf?(nil, options)
171
+ options[:paperWidth] = @paper_size[:width].to_f if @paper_size
172
+ options[:paperHeight] = @paper_size[:height].to_f if @paper_size
173
+ browser.pdf(encoding: :base64, **options)
174
+ else
175
+ browser.screenshot(format: format, encoding: :base64, **options)
176
+ end
144
177
  end
145
178
 
146
- def paper_size=(size = {})
147
- browser.set_paper_size(size)
179
+ def zoom_factor=(value)
180
+ @zoom_factor = value.to_f
148
181
  end
149
182
 
150
- def zoom_factor=(zoom_factor)
151
- browser.set_zoom_factor(zoom_factor)
183
+ def paper_size=(value)
184
+ @paper_size = value
152
185
  end
153
186
 
154
187
  def resize(width, height)
@@ -179,15 +212,25 @@ module Capybara::Cuprite
179
212
  end
180
213
 
181
214
  def scroll_to(left, top)
182
- browser.scroll_to(left, top)
215
+ browser.mouse.scroll_to(left, top)
183
216
  end
184
217
 
185
218
  def network_traffic(type = nil)
186
- browser.network_traffic(type)
219
+ traffic = browser.network.traffic
220
+
221
+ case type.to_s
222
+ when "all"
223
+ traffic
224
+ when "blocked"
225
+ traffic.select(&:blocked?)
226
+ else
227
+ # when request isn't blocked
228
+ traffic.reject(&:blocked?)
229
+ end
187
230
  end
188
231
 
189
232
  def clear_network_traffic
190
- browser.clear_network_traffic
233
+ browser.network.clear(:traffic)
191
234
  end
192
235
 
193
236
  def set_proxy(ip, port, type = nil, user = nil, password = nil, bypass = nil)
@@ -195,31 +238,31 @@ module Capybara::Cuprite
195
238
  server = type ? "#{type}=#{ip}:#{port}" : "#{ip}:#{port}"
196
239
  @options[:browser_options].merge!("proxy-server" => server)
197
240
  @options[:browser_options].merge!("proxy-bypass-list" => bypass) if bypass
198
- browser.proxy_authorize(user, password)
241
+ browser.network.authorize(type: :proxy, user: user, password: password)
199
242
  end
200
243
 
201
244
  def headers
202
- browser.headers
245
+ browser.headers.get
203
246
  end
204
247
 
205
248
  def headers=(headers)
206
- browser.headers=(headers)
249
+ browser.headers.set(headers)
207
250
  end
208
251
 
209
252
  def add_headers(headers)
210
- browser.add_headers(headers)
253
+ browser.headers.add(headers)
211
254
  end
212
255
 
213
256
  def add_header(name, value, permanent: true)
214
- browser.add_header({ name => value }, permanent: permanent)
257
+ browser.headers.add({ name => value }, permanent: permanent)
215
258
  end
216
259
 
217
260
  def response_headers
218
- browser.response_headers
261
+ browser.network.response&.headers
219
262
  end
220
263
 
221
264
  def cookies
222
- browser.cookies
265
+ browser.cookies.all
223
266
  end
224
267
 
225
268
  def set_cookie(name, value, options = {})
@@ -227,31 +270,37 @@ module Capybara::Cuprite
227
270
  options[:name] ||= name
228
271
  options[:value] ||= value
229
272
  options[:domain] ||= default_domain
230
-
231
- expires = options.delete(:expires).to_i
232
- options[:expires] = expires if expires > 0
233
-
234
- browser.set_cookie(options)
273
+ browser.cookies.set(**options)
235
274
  end
236
275
 
237
276
  def remove_cookie(name, **options)
238
277
  options[:domain] = default_domain if options.empty?
239
- browser.remove_cookie(options.merge(name: name))
278
+ browser.cookies.remove(**options.merge(name: name))
240
279
  end
241
280
 
242
281
  def clear_cookies
243
- browser.clear_cookies
282
+ browser.cookies.clear
244
283
  end
245
284
 
246
285
  def clear_memory_cache
247
- browser.clear_memory_cache
286
+ browser.network.clear(:cache)
248
287
  end
249
288
 
250
289
  def basic_authorize(user, password)
251
- browser.authorize(user, password)
290
+ browser.network.authorize(user: user, password: password)
252
291
  end
253
292
  alias_method :authorize, :basic_authorize
254
293
 
294
+ def debug
295
+ if @options[:inspector]
296
+ Process.spawn(browser.process.path, "http://#{browser.process.host}:#{browser.process.port}")
297
+ pause
298
+ else
299
+ raise Error, "To use the remote debugging, you have to launch " \
300
+ "the driver with `inspector: true` configuration option"
301
+ end
302
+ end
303
+
255
304
  def pause
256
305
  # STDIN is not necessarily connected to a keyboard. It might even be closed.
257
306
  # So we need a method other than keypress to continue.
@@ -284,15 +333,17 @@ module Capybara::Cuprite
284
333
  end
285
334
 
286
335
  def invalid_element_errors
287
- [Capybara::Cuprite::ObsoleteNode, Capybara::Cuprite::MouseEventFailed]
336
+ [Capybara::Cuprite::ObsoleteNode,
337
+ Capybara::Cuprite::MouseEventFailed,
338
+ Ferrum::NoExecutionContextError]
288
339
  end
289
340
 
290
341
  def go_back
291
- browser.go_back
342
+ browser.back
292
343
  end
293
344
 
294
345
  def go_forward
295
- browser.go_forward
346
+ browser.forward
296
347
  end
297
348
 
298
349
  def refresh
@@ -336,11 +387,7 @@ module Capybara::Cuprite
336
387
  end
337
388
 
338
389
  def native_args(args)
339
- args.map { |arg| arg.is_a?(Capybara::Cuprite::Node) ? arg.native : arg }
340
- end
341
-
342
- def screen_size
343
- @options[:screen_size] || [1366, 768]
390
+ args.map { |arg| arg.is_a?(Capybara::Cuprite::Node) ? arg.node : arg }
344
391
  end
345
392
 
346
393
  def session_wait_time
@@ -368,11 +415,17 @@ module Capybara::Cuprite
368
415
  when Array
369
416
  arg.map { |e| unwrap_script_result(e) }
370
417
  when Hash
371
- return Capybara::Cuprite::Node.new(self, arg["target_id"], arg["node"]) if arg["target_id"]
372
418
  arg.each { |k, v| arg[k] = unwrap_script_result(v) }
419
+ when Ferrum::Node
420
+ Node.new(self, arg)
373
421
  else
374
422
  arg
375
423
  end
376
424
  end
425
+
426
+ def pdf?(path, options)
427
+ (path && File.extname(path).delete(".") == "pdf") ||
428
+ options[:format].to_s == "pdf"
429
+ end
377
430
  end
378
431
  end
@@ -3,7 +3,6 @@
3
3
  module Capybara
4
4
  module Cuprite
5
5
  class Error < StandardError; end
6
- class NoSuchWindowError < Error; end
7
6
 
8
7
  class ClientError < Error
9
8
  attr_reader :response
@@ -13,45 +12,6 @@ module Capybara
13
12
  end
14
13
  end
15
14
 
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
15
  class InvalidSelector < ClientError
56
16
  def initialize(response, method, selector)
57
17
  super(response)
@@ -82,16 +42,14 @@ module Capybara
82
42
  end
83
43
  end
84
44
 
85
- class NodeError < ClientError
45
+ class ObsoleteNode < ClientError
86
46
  attr_reader :node
87
47
 
88
48
  def initialize(node, response)
89
49
  @node = node
90
50
  super(response)
91
51
  end
92
- end
93
52
 
94
- class ObsoleteNode < NodeError
95
53
  def message
96
54
  "The element you are trying to interact with is either not part of the DOM, or is " \
97
55
  "not currently visible on the page (perhaps display: none is set). " \
@@ -100,26 +58,5 @@ module Capybara
100
58
  "new element."
101
59
  end
102
60
  end
103
-
104
- class TimeoutError < Error
105
- def message
106
- "Timed out waiting for response. It's possible that this happened " \
107
- "because something took a very long time (for example a page load " \
108
- "was slow). If so, setting the Cuprite :timeout option to a higher " \
109
- "value might help."
110
- end
111
- end
112
-
113
- class ScriptTimeoutError < Error
114
- def message
115
- "Timed out waiting for evaluated script to resturn a value"
116
- end
117
- end
118
-
119
- class DeadBrowser < Error
120
- def initialize(message = "Chrome is dead")
121
- super
122
- end
123
- end
124
61
  end
125
62
  end
@@ -123,13 +123,17 @@ class Cuprite {
123
123
  value = value.substr(0, node.maxLength);
124
124
  }
125
125
 
126
+ let valueBefore = node.value;
127
+
126
128
  this.trigger(node, "focus");
127
129
  this.setValue(node, "");
128
130
 
129
131
  if (node.type == "number" || node.type == "date") {
130
132
  this.setValue(node, value);
133
+ this.input(node);
131
134
  } else if (node.type == "time") {
132
135
  this.setValue(node, new Date(value).toTimeString().split(" ")[0]);
136
+ this.input(node);
133
137
  } else if (node.type == "datetime-local") {
134
138
  value = new Date(value);
135
139
  let year = value.getFullYear();
@@ -139,20 +143,29 @@ class Cuprite {
139
143
  let min = ("0" + value.getMinutes()).slice(-2);
140
144
  let sec = ("0" + value.getSeconds()).slice(-2);
141
145
  this.setValue(node, `${year}-${month}-${date}T${hour}:${min}:${sec}`);
146
+ this.input(node);
142
147
  } else {
143
148
  for (let i = 0; i < value.length; i++) {
144
149
  let char = value[i];
145
150
  let keyCode = this.characterToKeyCode(char);
146
- this.keyupdowned(node, "keydown", keyCode);
147
- this.setValue(node, node.value + char);
151
+ // call the following functions in order, if one returns false (preventDefault),
152
+ // stop the call chain
153
+ [
154
+ () => this.keyupdowned(node, "keydown", keyCode),
155
+ () => this.keypressed(node, false, false, false, false, char.charCodeAt(0), char.charCodeAt(0)),
156
+ () => {
157
+ this.setValue(node, node.value + char)
158
+ this.input(node)
159
+ }
160
+ ].some(fn => fn())
148
161
 
149
- this.keypressed(node, false, false, false, false, char.charCodeAt(0), char.charCodeAt(0));
150
162
  this.keyupdowned(node, "keyup", keyCode);
151
163
  }
152
164
  }
153
165
 
154
- this.changed(node);
155
- this.input(node);
166
+ if (valueBefore !== node.value) {
167
+ this.changed(node);
168
+ }
156
169
  this.trigger(node, "blur");
157
170
  }
158
171
 
@@ -172,14 +185,20 @@ class Cuprite {
172
185
  node.dispatchEvent(event);
173
186
  }
174
187
 
188
+ /**
189
+ * @return {boolean} false when an event handler called preventDefault()
190
+ */
175
191
  keyupdowned(node, eventName, keyCode) {
176
192
  let event = document.createEvent("UIEvents");
177
193
  event.initEvent(eventName, true, true);
178
194
  event.keyCode = keyCode;
179
195
  event.charCode = 0;
180
- node.dispatchEvent(event);
196
+ return !node.dispatchEvent(event);
181
197
  }
182
198
 
199
+ /**
200
+ * @return {boolean} false when an event handler called preventDefault()
201
+ */
183
202
  keypressed(node, altKey, ctrlKey, shiftKey, metaKey, keyCode, charCode) {
184
203
  event = document.createEvent("UIEvents");
185
204
  event.initEvent("keypress", true, true);
@@ -190,7 +209,7 @@ class Cuprite {
190
209
  event.metaKey = metaKey;
191
210
  event.keyCode = keyCode;
192
211
  event.charCode = charCode;
193
- node.dispatchEvent(event);
212
+ return !node.dispatchEvent(event);
194
213
  }
195
214
 
196
215
  characterToKeyCode(char) {
@@ -450,19 +469,6 @@ class Cuprite {
450
469
  return node.contains(selectedNode);
451
470
  }
452
471
 
453
- isCyclic(object) {
454
- if (Array.isArray(object) && object.every(n => n instanceof Node)) {
455
- return false;
456
- }
457
-
458
- try {
459
- this._json.stringify(object);
460
- return false;
461
- } catch (e) {
462
- return true;
463
- }
464
- }
465
-
466
472
  // This command is purely for testing error handling
467
473
  browserError() {
468
474
  throw new Error("zomg");
@@ -1,24 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "forwardable"
4
+
3
5
  module Capybara::Cuprite
4
6
  class Node < Capybara::Driver::Node
5
- attr_reader :target_id, :node
7
+ attr_reader :node
6
8
 
7
- def initialize(driver, target_id, node)
8
- super(driver, self)
9
- @target_id, @node = target_id, node
10
- end
9
+ extend Forwardable
11
10
 
12
- def browser
13
- driver.browser
11
+ delegate %i(description) => :node
12
+ delegate %i(browser) => :driver
13
+
14
+ def initialize(driver, node)
15
+ super(driver, self)
16
+ @node = node
14
17
  end
15
18
 
16
19
  def command(name, *args)
17
- browser.send(name, @node, *args)
18
- rescue BrowserError => e
20
+ browser.send(name, node, *args)
21
+ rescue Ferrum::NodeNotFoundError => e
22
+ raise ObsoleteNode.new(self, e.response)
23
+ rescue Ferrum::BrowserError => e
19
24
  case e.message
20
- when "No node with given id found"
21
- raise ObsoleteNode.new(self, e.response)
22
25
  when "Cuprite.MouseEventFailed"
23
26
  raise MouseEventFailed.new(self, e.response)
24
27
  else
@@ -28,13 +31,7 @@ module Capybara::Cuprite
28
31
 
29
32
  def parents
30
33
  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)
34
+ self.class.new(driver, parent)
38
35
  end
39
36
  end
40
37
 
@@ -46,6 +43,12 @@ module Capybara::Cuprite
46
43
  find(:css, selector)
47
44
  end
48
45
 
46
+ def find(method, selector)
47
+ command(:find_within, method, selector).map do |node|
48
+ self.class.new(driver, node)
49
+ end
50
+ end
51
+
49
52
  def all_text
50
53
  filter_text(command(:all_text))
51
54
  end
@@ -69,7 +72,8 @@ module Capybara::Cuprite
69
72
  def [](name)
70
73
  # Although the attribute matters, the property is consistent. Return that in
71
74
  # preference to the attribute for links and images.
72
- if ((tag_name == "img") && (name == "src")) || ((tag_name == "a") && (name == "href"))
75
+ if (tag_name == "img" && name == "src") ||
76
+ (tag_name == "a" && name == "href")
73
77
  # if attribute exists get the property
74
78
  return command(:attribute, name) && command(:property, name)
75
79
  end
@@ -121,7 +125,7 @@ module Capybara::Cuprite
121
125
  end
122
126
 
123
127
  def tag_name
124
- @tag_name ||= @node["nodeName"].downcase
128
+ @tag_name ||= description["nodeName"].downcase
125
129
  end
126
130
 
127
131
  def visible?
@@ -141,15 +145,15 @@ module Capybara::Cuprite
141
145
  end
142
146
 
143
147
  def click(keys = [], offset = {})
144
- command(:click, keys, offset)
148
+ prepare_and_click(:left, __method__, keys, offset)
145
149
  end
146
150
 
147
151
  def right_click(keys = [], offset = {})
148
- command(:right_click, keys, offset)
152
+ prepare_and_click(:right, __method__, keys, offset)
149
153
  end
150
154
 
151
155
  def double_click(keys = [], offset = {})
152
- command(:double_click, keys, offset)
156
+ prepare_and_click(:double, __method__, keys, offset)
153
157
  end
154
158
 
155
159
  def hover
@@ -157,7 +161,7 @@ module Capybara::Cuprite
157
161
  end
158
162
 
159
163
  def drag_to(other)
160
- command(:drag, other.node)
164
+ command(:drag, other)
161
165
  end
162
166
 
163
167
  def drag_by(x, y)
@@ -192,10 +196,7 @@ module Capybara::Cuprite
192
196
  end
193
197
 
194
198
  def ==(other)
195
- # We compare backendNodeId because once nodeId is sent to frontend backend
196
- # never returns same nodeId sending 0. In other words frontend is
197
- # responsible for keeping track of node ids.
198
- @target_id == other.target_id && @node["backendNodeId"] == other.node["backendNodeId"]
199
+ node == other.native.node
199
200
  end
200
201
 
201
202
  def send_keys(*keys)
@@ -208,7 +209,7 @@ module Capybara::Cuprite
208
209
  end
209
210
 
210
211
  def inspect
211
- %(#<#{self.class} @target_id=#{@target_id.inspect} @node=#{@node.inspect}>)
212
+ %(#<#{self.class} @node=#{@node.inspect}>)
212
213
  end
213
214
 
214
215
  # @api private
@@ -218,12 +219,17 @@ module Capybara::Cuprite
218
219
 
219
220
  # @api private
220
221
  def as_json(*)
221
- # FIXME: Where this method is used and why attr is called id?
222
- { ELEMENT: { target_id: @target_id, id: @node } }
222
+ # FIXME: Where is this method used and why attr is called id?
223
+ { ELEMENT: { node: node, id: node.node_id } }
223
224
  end
224
225
 
225
226
  private
226
227
 
228
+ def prepare_and_click(mode, name, keys, offset)
229
+ command(:before_click, name, keys, offset)
230
+ node.click(mode: mode, keys: keys, offset: offset)
231
+ end
232
+
227
233
  def filter_text(text)
228
234
  if Capybara::VERSION.to_f < 3
229
235
  Capybara::Helpers.normalize_whitespace(text.to_s)