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,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