cuprite 0.14.1 → 0.14.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,428 @@
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] = File.expand_path(Capybara.save_path) 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
+ browser_options = @options.fetch(:browser_options, {})
210
+ browser_options = browser_options.merge("proxy-server" => "#{host}:#{port}")
211
+ browser_options = browser_options.merge("proxy-bypass-list" => bypass) if bypass
212
+ @options[:browser_options] = browser_options
213
+
214
+ return unless user && password
215
+
216
+ browser.network.authorize(user: user, password: password, type: :proxy) do |request, _index, _total|
217
+ request.continue
218
+ end
219
+ end
220
+
221
+ def headers
222
+ browser.headers.get
223
+ end
224
+
225
+ def headers=(headers)
226
+ browser.headers.set(headers)
227
+ end
228
+
229
+ def add_headers(headers)
230
+ browser.headers.add(headers)
231
+ end
232
+
233
+ def add_header(name, value, permanent: true)
234
+ browser.headers.add({ name => value }, permanent: permanent)
235
+ end
236
+
237
+ def response_headers
238
+ browser.network.response&.headers
239
+ end
240
+
241
+ def cookies
242
+ browser.cookies.all
243
+ end
244
+
245
+ def set_cookie(name, value, options = {})
246
+ options = options.dup
247
+ options[:name] ||= name
248
+ options[:value] ||= value
249
+ options[:domain] ||= default_domain
250
+ browser.cookies.set(**options)
251
+ end
252
+
253
+ def remove_cookie(name, **options)
254
+ options[:domain] = default_domain if options.empty?
255
+ browser.cookies.remove(**options.merge(name: name))
256
+ end
257
+
258
+ def clear_cookies
259
+ browser.cookies.clear
260
+ end
261
+
262
+ def wait_for_network_idle(**options)
263
+ browser.network.wait_for_idle(**options)
264
+ end
265
+
266
+ def clear_memory_cache
267
+ browser.network.clear(:cache)
268
+ end
269
+
270
+ def basic_authorize(user, password)
271
+ browser.network.authorize(user: user, password: password) do |request, _index, _total|
272
+ request.continue
273
+ end
274
+ end
275
+ alias authorize basic_authorize
276
+
277
+ def debug_url
278
+ "http://#{browser.process.host}:#{browser.process.port}"
279
+ end
280
+
281
+ def debug(binding = nil)
282
+ if @options[:inspector]
283
+ Process.spawn(browser.process.path, debug_url)
284
+
285
+ if binding.respond_to?(:pry)
286
+ Pry.start(binding)
287
+ elsif binding.respond_to?(:irb)
288
+ binding.irb
289
+ else
290
+ pause
291
+ end
292
+ else
293
+ raise Error, "To use the remote debugging, you have to launch " \
294
+ "the driver with `inspector: ENV['INSPECTOR']` " \
295
+ "configuration option and run your test suite passing " \
296
+ "env variable"
297
+ end
298
+ end
299
+
300
+ def pause
301
+ # STDIN is not necessarily connected to a keyboard. It might even be closed.
302
+ # So we need a method other than keypress to continue.
303
+
304
+ # In jRuby - STDIN returns immediately from select
305
+ # see https://github.com/jruby/jruby/issues/1783
306
+ read, write = IO.pipe
307
+ thread = Thread.new do
308
+ IO.copy_stream($stdin, write)
309
+ write.close
310
+ end
311
+
312
+ warn "Cuprite execution paused. Press enter (or run 'kill -CONT #{Process.pid}') to continue."
313
+
314
+ signal = false
315
+ old_trap = trap("SIGCONT") do
316
+ signal = true
317
+ warn "\nSignal SIGCONT received"
318
+ end
319
+ keyboard = read.wait_readable(1) until keyboard || signal # wait for data on STDIN or signal SIGCONT received
320
+
321
+ unless signal
322
+ begin
323
+ input = read.read_nonblock(80) # clear out the read buffer
324
+ puts unless input&.end_with?("\n")
325
+ rescue EOFError, IO::WaitReadable
326
+ # Ignore problems reading from STDIN.
327
+ end
328
+ end
329
+ ensure
330
+ thread.kill
331
+ read.close
332
+ trap("SIGCONT", old_trap) # Restore the previous signal handler, if there was one.
333
+ warn "Continuing"
334
+ end
335
+
336
+ def wait?
337
+ true
338
+ end
339
+
340
+ def invalid_element_errors
341
+ [Capybara::Cuprite::ObsoleteNode,
342
+ Capybara::Cuprite::MouseEventFailed,
343
+ Ferrum::CoordinatesNotFoundError,
344
+ Ferrum::NoExecutionContextError,
345
+ Ferrum::NodeNotFoundError]
346
+ end
347
+
348
+ def accept_modal(type, options = {})
349
+ case type
350
+ when :alert, :confirm
351
+ browser.accept_confirm
352
+ when :prompt
353
+ browser.accept_prompt(options[:with])
354
+ end
355
+
356
+ yield if block_given?
357
+
358
+ browser.find_modal(options)
359
+ end
360
+
361
+ def dismiss_modal(type, options = {})
362
+ case type
363
+ when :confirm
364
+ browser.dismiss_confirm
365
+ when :prompt
366
+ browser.dismiss_prompt
367
+ end
368
+
369
+ yield if block_given?
370
+
371
+ browser.find_modal(options)
372
+ end
373
+
374
+ private
375
+
376
+ def default_domain
377
+ if @started
378
+ URI.parse(browser.current_url).host
379
+ else
380
+ URI.parse(default_cookie_host).host || "127.0.0.1"
381
+ end
382
+ end
383
+
384
+ def native_args(args)
385
+ args.map { |arg| arg.is_a?(Capybara::Cuprite::Node) ? arg.node : arg }
386
+ end
387
+
388
+ def session_wait_time
389
+ if respond_to?(:session_options)
390
+ session_options.default_max_wait_time
391
+ else
392
+ begin
393
+ Capybara.default_max_wait_time
394
+ rescue StandardError
395
+ Capybara.default_wait_time
396
+ end
397
+ end
398
+ end
399
+
400
+ def default_cookie_host
401
+ if respond_to?(:session_options)
402
+ session_options.app_host
403
+ else
404
+ Capybara.app_host
405
+ end || ""
406
+ end
407
+
408
+ def unwrap_script_result(arg)
409
+ case arg
410
+ when Array
411
+ arg.map { |e| unwrap_script_result(e) }
412
+ when Hash
413
+ arg.each { |k, v| arg[k] = unwrap_script_result(v) }
414
+ when Ferrum::Node
415
+ Node.new(self, arg)
416
+ else
417
+ arg
418
+ end
419
+ end
420
+
421
+ def pdf?(path, options)
422
+ (path && File.extname(path).delete(".") == "pdf") ||
423
+ options[:format].to_s == "pdf"
424
+ end
425
+ end
426
+ # rubocop:enable Metrics/ClassLength
427
+ end
428
+ 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