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,424 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "uri"
|
4
|
+
require "forwardable"
|
5
|
+
|
6
|
+
module Capybara
|
7
|
+
module Cuprite
|
8
|
+
# rubocop:disable Metrics/ClassLength
|
9
|
+
class Driver < Capybara::Driver::Base
|
10
|
+
DEFAULT_MAXIMIZE_SCREEN_SIZE = [1366, 768].freeze
|
11
|
+
EXTENSION = File.expand_path("javascripts/index.js", __dir__)
|
12
|
+
|
13
|
+
extend Forwardable
|
14
|
+
|
15
|
+
delegate %i[restart quit status_code timeout timeout= current_url title body
|
16
|
+
window_handles close_window switch_to_window within_window window_handle
|
17
|
+
back forward refresh wait_for_reload] => :browser
|
18
|
+
alias html body
|
19
|
+
alias current_window_handle window_handle
|
20
|
+
alias go_back back
|
21
|
+
alias go_forward forward
|
22
|
+
|
23
|
+
attr_reader :app, :options, :screen_size
|
24
|
+
|
25
|
+
def initialize(app, options = {})
|
26
|
+
@app = app
|
27
|
+
@options = options.dup
|
28
|
+
@started = false
|
29
|
+
|
30
|
+
@options[:extensions] ||= []
|
31
|
+
@options[:extensions] << EXTENSION
|
32
|
+
|
33
|
+
@screen_size = @options.delete(:screen_size)
|
34
|
+
@screen_size ||= DEFAULT_MAXIMIZE_SCREEN_SIZE
|
35
|
+
|
36
|
+
@options[:save_path] = Capybara.save_path.to_s if Capybara.save_path
|
37
|
+
|
38
|
+
ENV["FERRUM_DEBUG"] = "true" if ENV["CUPRITE_DEBUG"]
|
39
|
+
|
40
|
+
super()
|
41
|
+
end
|
42
|
+
|
43
|
+
def needs_server?
|
44
|
+
true
|
45
|
+
end
|
46
|
+
|
47
|
+
def browser
|
48
|
+
@browser ||= Browser.new(@options)
|
49
|
+
end
|
50
|
+
|
51
|
+
def visit(url)
|
52
|
+
@started = true
|
53
|
+
browser.visit(url)
|
54
|
+
end
|
55
|
+
|
56
|
+
def frame_url
|
57
|
+
evaluate_script("window.location.href")
|
58
|
+
end
|
59
|
+
|
60
|
+
def source
|
61
|
+
browser.source.to_s
|
62
|
+
end
|
63
|
+
|
64
|
+
def frame_title
|
65
|
+
evaluate_script("document.title")
|
66
|
+
end
|
67
|
+
|
68
|
+
def find_xpath(selector)
|
69
|
+
find(:xpath, selector)
|
70
|
+
end
|
71
|
+
|
72
|
+
def find_css(selector)
|
73
|
+
find(:css, selector)
|
74
|
+
end
|
75
|
+
|
76
|
+
def find(method, selector)
|
77
|
+
browser.find(method, selector).map { |native| Node.new(self, native) }
|
78
|
+
end
|
79
|
+
|
80
|
+
def click(x, y)
|
81
|
+
browser.mouse.click(x: x, y: y)
|
82
|
+
end
|
83
|
+
|
84
|
+
def evaluate_script(script, *args)
|
85
|
+
result = browser.evaluate(script, *native_args(args))
|
86
|
+
unwrap_script_result(result)
|
87
|
+
end
|
88
|
+
|
89
|
+
def evaluate_async_script(script, *args)
|
90
|
+
result = browser.evaluate_async(script, session_wait_time, *native_args(args))
|
91
|
+
unwrap_script_result(result)
|
92
|
+
end
|
93
|
+
|
94
|
+
def execute_script(script, *args)
|
95
|
+
browser.execute(script, *native_args(args))
|
96
|
+
nil
|
97
|
+
end
|
98
|
+
|
99
|
+
def switch_to_frame(locator)
|
100
|
+
handle = case locator
|
101
|
+
when Capybara::Node::Element
|
102
|
+
locator.native.description["frameId"]
|
103
|
+
when :parent, :top
|
104
|
+
locator
|
105
|
+
end
|
106
|
+
|
107
|
+
browser.switch_to_frame(handle)
|
108
|
+
end
|
109
|
+
|
110
|
+
def open_new_window
|
111
|
+
target = browser.default_context.create_target
|
112
|
+
target.maybe_sleep_if_new_window
|
113
|
+
target.page = Page.new(target.id, browser)
|
114
|
+
target.page
|
115
|
+
end
|
116
|
+
|
117
|
+
def no_such_window_error
|
118
|
+
Ferrum::NoSuchPageError
|
119
|
+
end
|
120
|
+
|
121
|
+
def reset!
|
122
|
+
@zoom_factor = nil
|
123
|
+
@paper_size = nil
|
124
|
+
browser.url_blacklist = @options[:url_blacklist]
|
125
|
+
browser.url_whitelist = @options[:url_whitelist]
|
126
|
+
browser.reset
|
127
|
+
@started = false
|
128
|
+
end
|
129
|
+
|
130
|
+
def save_screenshot(path, options = {})
|
131
|
+
options[:scale] = @zoom_factor if @zoom_factor
|
132
|
+
|
133
|
+
if pdf?(path, options)
|
134
|
+
options[:paperWidth] = @paper_size[:width].to_f if @paper_size
|
135
|
+
options[:paperHeight] = @paper_size[:height].to_f if @paper_size
|
136
|
+
browser.pdf(path: path, **options)
|
137
|
+
else
|
138
|
+
browser.screenshot(path: path, **options)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
alias render save_screenshot
|
142
|
+
|
143
|
+
def render_base64(format = :png, options = {})
|
144
|
+
if pdf?(nil, options)
|
145
|
+
options[:paperWidth] = @paper_size[:width].to_f if @paper_size
|
146
|
+
options[:paperHeight] = @paper_size[:height].to_f if @paper_size
|
147
|
+
browser.pdf(encoding: :base64, **options)
|
148
|
+
else
|
149
|
+
browser.screenshot(format: format, encoding: :base64, **options)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def zoom_factor=(value)
|
154
|
+
@zoom_factor = value.to_f
|
155
|
+
end
|
156
|
+
|
157
|
+
attr_writer :paper_size
|
158
|
+
|
159
|
+
def resize(width, height)
|
160
|
+
browser.resize(width: width, height: height)
|
161
|
+
end
|
162
|
+
alias resize_window resize
|
163
|
+
|
164
|
+
def resize_window_to(handle, width, height)
|
165
|
+
within_window(handle) do
|
166
|
+
resize(width, height)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def maximize_window(handle)
|
171
|
+
resize_window_to(handle, *screen_size)
|
172
|
+
end
|
173
|
+
|
174
|
+
def window_size(handle)
|
175
|
+
within_window(handle) do
|
176
|
+
evaluate_script("[window.innerWidth, window.innerHeight]")
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def fullscreen_window(handle)
|
181
|
+
within_window(handle) do
|
182
|
+
browser.resize(fullscreen: true)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def scroll_to(left, top)
|
187
|
+
browser.mouse.scroll_to(left, top)
|
188
|
+
end
|
189
|
+
|
190
|
+
def network_traffic(type = nil)
|
191
|
+
traffic = browser.network.traffic
|
192
|
+
|
193
|
+
case type.to_s
|
194
|
+
when "all"
|
195
|
+
traffic
|
196
|
+
when "blocked"
|
197
|
+
traffic.select(&:blocked?)
|
198
|
+
else
|
199
|
+
# when request isn't blocked
|
200
|
+
traffic.reject(&:blocked?)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def clear_network_traffic
|
205
|
+
browser.network.clear(:traffic)
|
206
|
+
end
|
207
|
+
|
208
|
+
def set_proxy(host, port, user = nil, password = nil, bypass = nil)
|
209
|
+
@options[:browser_options] ||= {}
|
210
|
+
@options[:browser_options].merge!("proxy-server" => "#{host}:#{port}")
|
211
|
+
@options[:browser_options].merge!("proxy-bypass-list" => bypass) if bypass
|
212
|
+
browser.network.authorize(type: :proxy, user: user, password: password) do |request, _index, _total|
|
213
|
+
request.continue
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def headers
|
218
|
+
browser.headers.get
|
219
|
+
end
|
220
|
+
|
221
|
+
def headers=(headers)
|
222
|
+
browser.headers.set(headers)
|
223
|
+
end
|
224
|
+
|
225
|
+
def add_headers(headers)
|
226
|
+
browser.headers.add(headers)
|
227
|
+
end
|
228
|
+
|
229
|
+
def add_header(name, value, permanent: true)
|
230
|
+
browser.headers.add({ name => value }, permanent: permanent)
|
231
|
+
end
|
232
|
+
|
233
|
+
def response_headers
|
234
|
+
browser.network.response&.headers
|
235
|
+
end
|
236
|
+
|
237
|
+
def cookies
|
238
|
+
browser.cookies.all
|
239
|
+
end
|
240
|
+
|
241
|
+
def set_cookie(name, value, options = {})
|
242
|
+
options = options.dup
|
243
|
+
options[:name] ||= name
|
244
|
+
options[:value] ||= value
|
245
|
+
options[:domain] ||= default_domain
|
246
|
+
browser.cookies.set(**options)
|
247
|
+
end
|
248
|
+
|
249
|
+
def remove_cookie(name, **options)
|
250
|
+
options[:domain] = default_domain if options.empty?
|
251
|
+
browser.cookies.remove(**options.merge(name: name))
|
252
|
+
end
|
253
|
+
|
254
|
+
def clear_cookies
|
255
|
+
browser.cookies.clear
|
256
|
+
end
|
257
|
+
|
258
|
+
def wait_for_network_idle(**options)
|
259
|
+
browser.network.wait_for_idle(**options)
|
260
|
+
end
|
261
|
+
|
262
|
+
def clear_memory_cache
|
263
|
+
browser.network.clear(:cache)
|
264
|
+
end
|
265
|
+
|
266
|
+
def basic_authorize(user, password)
|
267
|
+
browser.network.authorize(user: user, password: password) do |request, _index, _total|
|
268
|
+
request.continue
|
269
|
+
end
|
270
|
+
end
|
271
|
+
alias authorize basic_authorize
|
272
|
+
|
273
|
+
def debug_url
|
274
|
+
"http://#{browser.process.host}:#{browser.process.port}"
|
275
|
+
end
|
276
|
+
|
277
|
+
def debug(binding = nil)
|
278
|
+
if @options[:inspector]
|
279
|
+
Process.spawn(browser.process.path, debug_url)
|
280
|
+
|
281
|
+
if binding.respond_to?(:pry)
|
282
|
+
Pry.start(binding)
|
283
|
+
elsif binding.respond_to?(:irb)
|
284
|
+
binding.irb
|
285
|
+
else
|
286
|
+
pause
|
287
|
+
end
|
288
|
+
else
|
289
|
+
raise Error, "To use the remote debugging, you have to launch " \
|
290
|
+
"the driver with `inspector: ENV['INSPECTOR']` " \
|
291
|
+
"configuration option and run your test suite passing " \
|
292
|
+
"env variable"
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
def pause
|
297
|
+
# STDIN is not necessarily connected to a keyboard. It might even be closed.
|
298
|
+
# So we need a method other than keypress to continue.
|
299
|
+
|
300
|
+
# In jRuby - STDIN returns immediately from select
|
301
|
+
# see https://github.com/jruby/jruby/issues/1783
|
302
|
+
read, write = IO.pipe
|
303
|
+
thread = Thread.new do
|
304
|
+
IO.copy_stream($stdin, write)
|
305
|
+
write.close
|
306
|
+
end
|
307
|
+
|
308
|
+
warn "Cuprite execution paused. Press enter (or run 'kill -CONT #{Process.pid}') to continue."
|
309
|
+
|
310
|
+
signal = false
|
311
|
+
old_trap = trap("SIGCONT") do
|
312
|
+
signal = true
|
313
|
+
warn "\nSignal SIGCONT received"
|
314
|
+
end
|
315
|
+
keyboard = read.wait_readable(1) until keyboard || signal # wait for data on STDIN or signal SIGCONT received
|
316
|
+
|
317
|
+
unless signal
|
318
|
+
begin
|
319
|
+
input = read.read_nonblock(80) # clear out the read buffer
|
320
|
+
puts unless input&.end_with?("\n")
|
321
|
+
rescue EOFError, IO::WaitReadable
|
322
|
+
# Ignore problems reading from STDIN.
|
323
|
+
end
|
324
|
+
end
|
325
|
+
ensure
|
326
|
+
thread.kill
|
327
|
+
read.close
|
328
|
+
trap("SIGCONT", old_trap) # Restore the previous signal handler, if there was one.
|
329
|
+
warn "Continuing"
|
330
|
+
end
|
331
|
+
|
332
|
+
def wait?
|
333
|
+
true
|
334
|
+
end
|
335
|
+
|
336
|
+
def invalid_element_errors
|
337
|
+
[Capybara::Cuprite::ObsoleteNode,
|
338
|
+
Capybara::Cuprite::MouseEventFailed,
|
339
|
+
Ferrum::CoordinatesNotFoundError,
|
340
|
+
Ferrum::NoExecutionContextError,
|
341
|
+
Ferrum::NodeNotFoundError]
|
342
|
+
end
|
343
|
+
|
344
|
+
def accept_modal(type, options = {})
|
345
|
+
case type
|
346
|
+
when :alert, :confirm
|
347
|
+
browser.accept_confirm
|
348
|
+
when :prompt
|
349
|
+
browser.accept_prompt(options[:with])
|
350
|
+
end
|
351
|
+
|
352
|
+
yield if block_given?
|
353
|
+
|
354
|
+
browser.find_modal(options)
|
355
|
+
end
|
356
|
+
|
357
|
+
def dismiss_modal(type, options = {})
|
358
|
+
case type
|
359
|
+
when :confirm
|
360
|
+
browser.dismiss_confirm
|
361
|
+
when :prompt
|
362
|
+
browser.dismiss_prompt
|
363
|
+
end
|
364
|
+
|
365
|
+
yield if block_given?
|
366
|
+
|
367
|
+
browser.find_modal(options)
|
368
|
+
end
|
369
|
+
|
370
|
+
private
|
371
|
+
|
372
|
+
def default_domain
|
373
|
+
if @started
|
374
|
+
URI.parse(browser.current_url).host
|
375
|
+
else
|
376
|
+
URI.parse(default_cookie_host).host || "127.0.0.1"
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
def native_args(args)
|
381
|
+
args.map { |arg| arg.is_a?(Capybara::Cuprite::Node) ? arg.node : arg }
|
382
|
+
end
|
383
|
+
|
384
|
+
def session_wait_time
|
385
|
+
if respond_to?(:session_options)
|
386
|
+
session_options.default_max_wait_time
|
387
|
+
else
|
388
|
+
begin
|
389
|
+
Capybara.default_max_wait_time
|
390
|
+
rescue StandardError
|
391
|
+
Capybara.default_wait_time
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
def default_cookie_host
|
397
|
+
if respond_to?(:session_options)
|
398
|
+
session_options.app_host
|
399
|
+
else
|
400
|
+
Capybara.app_host
|
401
|
+
end || ""
|
402
|
+
end
|
403
|
+
|
404
|
+
def unwrap_script_result(arg)
|
405
|
+
case arg
|
406
|
+
when Array
|
407
|
+
arg.map { |e| unwrap_script_result(e) }
|
408
|
+
when Hash
|
409
|
+
arg.each { |k, v| arg[k] = unwrap_script_result(v) }
|
410
|
+
when Ferrum::Node
|
411
|
+
Node.new(self, arg)
|
412
|
+
else
|
413
|
+
arg
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
def pdf?(path, options)
|
418
|
+
(path && File.extname(path).delete(".") == "pdf") ||
|
419
|
+
options[:format].to_s == "pdf"
|
420
|
+
end
|
421
|
+
end
|
422
|
+
# rubocop:enable Metrics/ClassLength
|
423
|
+
end
|
424
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Capybara
|
4
|
+
module Cuprite
|
5
|
+
class Error < StandardError; end
|
6
|
+
|
7
|
+
class ClientError < Error
|
8
|
+
attr_reader :response
|
9
|
+
|
10
|
+
def initialize(response)
|
11
|
+
@response = response
|
12
|
+
super()
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class InvalidSelector < ClientError
|
17
|
+
def initialize(response, method, selector)
|
18
|
+
super(response)
|
19
|
+
@method = method
|
20
|
+
@selector = selector
|
21
|
+
end
|
22
|
+
|
23
|
+
def message
|
24
|
+
"Browser raised error trying to find #{@method}: #{@selector.inspect}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class MouseEventFailed < ClientError
|
29
|
+
attr_reader :name, :selector, :position
|
30
|
+
|
31
|
+
def initialize(*)
|
32
|
+
super
|
33
|
+
data = /\A\w+: (\w+), (.+?), ([\d.-]+), ([\d.-]+)/.match(@response)
|
34
|
+
@name, @selector = data.values_at(1, 2)
|
35
|
+
@position = data.values_at(3, 4).map(&:to_f)
|
36
|
+
end
|
37
|
+
|
38
|
+
def message
|
39
|
+
"Firing a #{name} at coordinates [#{position.join(', ')}] failed. Cuprite detected " \
|
40
|
+
"another element with CSS selector \"#{selector}\" at this position. " \
|
41
|
+
"It may be overlapping the element you are trying to interact with. " \
|
42
|
+
"If you don't care about overlapping elements, try using node.trigger(\"#{name}\")."
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class ObsoleteNode < ClientError
|
47
|
+
attr_reader :node
|
48
|
+
|
49
|
+
def initialize(node, response)
|
50
|
+
@node = node
|
51
|
+
super(response)
|
52
|
+
end
|
53
|
+
|
54
|
+
def message
|
55
|
+
"The element you are trying to interact with is either not part of the DOM, or is " \
|
56
|
+
"not currently visible on the page (perhaps display: none is set). " \
|
57
|
+
"It is possible the element has been replaced by another element and you meant to interact with " \
|
58
|
+
"the new element. If so you need to do a new find in order to get a reference to the " \
|
59
|
+
"new element."
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|