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