cuprite 0.2.0

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,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara::Cuprite
4
+ class Browser
5
+ class Targets
6
+ def initialize(browser)
7
+ @mutex = Mutex.new
8
+ @browser = browser
9
+ @_default = targets.first["targetId"]
10
+
11
+ @browser.subscribe("Target.detachedFromTarget") do |params|
12
+ page = remove_page(params["targetId"])
13
+ page&.close_connection
14
+ end
15
+
16
+ reset
17
+ end
18
+
19
+ def push(target_id, page = nil)
20
+ @targets[target_id] = page
21
+ end
22
+
23
+ def refresh
24
+ @mutex.synchronize do
25
+ targets.each { |t| push(t["targetId"]) if !default?(t) && !has?(t) }
26
+ end
27
+ end
28
+
29
+ def page
30
+ raise NoSuchWindowError unless @page
31
+ @page
32
+ end
33
+
34
+ def window_handle
35
+ page.target_id
36
+ end
37
+
38
+ def window_handles
39
+ @mutex.synchronize { @targets.keys }
40
+ end
41
+
42
+ def switch_to_window(target_id)
43
+ page = @targets[target_id]
44
+ page ||= Page.new(target_id, @browser)
45
+ @targets[target_id] ||= page
46
+ @page = page
47
+ end
48
+
49
+ def open_new_window
50
+ target_id = @browser.command("Target.createTarget", url: "about:blank", browserContextId: @_context_id)["targetId"]
51
+ page = Page.new(target_id, @browser)
52
+ push(target_id, page)
53
+ target_id
54
+ end
55
+
56
+ def close_window(target_id)
57
+ remove_page(target_id)&.close
58
+ end
59
+
60
+ def within_window(locator)
61
+ original = window_handle
62
+ if window_handles.include?(locator)
63
+ switch_to_window(locator)
64
+ yield
65
+ else
66
+ raise NoSuchWindowError
67
+ end
68
+ ensure
69
+ switch_to_window(original)
70
+ end
71
+
72
+ def reset
73
+ if @page
74
+ @page.close
75
+ @browser.command("Target.disposeBrowserContext", browserContextId: @_context_id)
76
+ end
77
+
78
+ @page = nil
79
+ @targets = {}
80
+ @_context_id = nil
81
+
82
+ @_context_id = @browser.command("Target.createBrowserContext")["browserContextId"]
83
+ target_id = @browser.command("Target.createTarget", url: "about:blank", browserContextId: @_context_id)["targetId"]
84
+ @page = Page.new(target_id, @browser)
85
+ push(target_id, @page)
86
+ end
87
+
88
+ private
89
+
90
+ def remove_page(target_id)
91
+ page = @targets.delete(target_id)
92
+ @page = nil if page && @page == page
93
+ page
94
+ end
95
+
96
+ def targets
97
+ @browser.command("Target.getTargets")["targetInfos"]
98
+ end
99
+
100
+ def default?(target)
101
+ @_default == target["targetId"]
102
+ end
103
+
104
+ def has?(target)
105
+ @targets.key?(target["targetId"])
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "socket"
5
+ require "forwardable"
6
+ require "websocket/driver"
7
+
8
+ module Capybara::Cuprite
9
+ class Browser
10
+ class WebSocket
11
+ extend Forwardable
12
+
13
+ delegate close: :@driver
14
+
15
+ attr_reader :url, :messages
16
+
17
+ def initialize(url, logger)
18
+ @url = url
19
+ @logger = logger
20
+ uri = URI.parse(@url)
21
+ @sock = TCPSocket.new(uri.host, uri.port)
22
+ @driver = ::WebSocket::Driver.client(self)
23
+ @messages = Queue.new
24
+
25
+ @driver.on(:message, &method(:on_message))
26
+
27
+ @thread = Thread.new do
28
+ begin
29
+ while data = @sock.readpartial(512)
30
+ @driver.parse(data)
31
+ end
32
+ rescue EOFError, Errno::ECONNRESET
33
+ @messages.close
34
+ end
35
+ end
36
+
37
+ @thread.priority = 1
38
+
39
+ @driver.start
40
+ end
41
+
42
+ def send_message(data)
43
+ @started_at ||= Time.now.to_f
44
+ json = data.to_json
45
+ @driver.text(json)
46
+ @logger&.puts("\n\n[#{Time.now.to_f - @started_at}] >>> #{json}")
47
+ end
48
+
49
+ def on_message(event)
50
+ data = JSON.parse(event.data)
51
+ @messages.push(data)
52
+ @logger&.puts("[#{Time.now.to_f - @started_at}] <<< #{event.data}\n")
53
+ end
54
+
55
+ def write(data)
56
+ @sock.write(data)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara::Cuprite
4
+ class Cookie
5
+ def initialize(attributes)
6
+ @attributes = attributes
7
+ end
8
+
9
+ def name
10
+ @attributes["name"]
11
+ end
12
+
13
+ def value
14
+ @attributes["value"]
15
+ end
16
+
17
+ def domain
18
+ @attributes["domain"]
19
+ end
20
+
21
+ def path
22
+ @attributes["path"]
23
+ end
24
+
25
+ def size
26
+ @attributes["size"]
27
+ end
28
+
29
+ def secure?
30
+ @attributes["secure"]
31
+ end
32
+
33
+ def httponly?
34
+ @attributes["httpOnly"]
35
+ end
36
+
37
+ def session?
38
+ @attributes["session"]
39
+ end
40
+
41
+ def expires
42
+ if @attributes["expires"] > 0
43
+ Time.at(@attributes["expires"])
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,396 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ require "forwardable"
6
+
7
+ module Capybara::Cuprite
8
+ class Driver < Capybara::Driver::Base
9
+ extend Forwardable
10
+
11
+ delegate %i(restart quit status_code timeout timeout=) => :browser
12
+
13
+ attr_reader :app, :options
14
+
15
+ def initialize(app, options = {})
16
+ @app = app
17
+ @options = options.freeze
18
+ @started = false
19
+ end
20
+
21
+ def needs_server?
22
+ true
23
+ end
24
+
25
+ def browser
26
+ @browser ||= Browser.start(@options)
27
+ end
28
+
29
+ def visit(url)
30
+ @started = true
31
+ browser.visit(url)
32
+ end
33
+
34
+ def current_url
35
+ if Capybara::VERSION.to_f < 3.0
36
+ frame_url
37
+ else
38
+ browser.current_url
39
+ end
40
+ end
41
+
42
+ def frame_url
43
+ browser.frame_url
44
+ end
45
+
46
+ def html
47
+ browser.body
48
+ end
49
+ alias_method :body, :html
50
+
51
+ def source
52
+ browser.source.to_s
53
+ end
54
+
55
+ def title
56
+ if Capybara::VERSION.to_f < 3.0
57
+ frame_title
58
+ else
59
+ browser.title
60
+ end
61
+ end
62
+
63
+ def frame_title
64
+ browser.frame_title
65
+ end
66
+
67
+ def find(method, selector)
68
+ browser.find(method, selector).map { |target_id, node| Node.new(self, target_id, node) }
69
+ end
70
+
71
+ def find_xpath(selector)
72
+ find :xpath, selector
73
+ end
74
+
75
+ def find_css(selector)
76
+ find :css, selector
77
+ end
78
+
79
+ def click(x, y)
80
+ browser.click_coordinates(x, y)
81
+ end
82
+
83
+ def evaluate_script(script, *args)
84
+ result = browser.evaluate(script, *native_args(args))
85
+ unwrap_script_result(result)
86
+ end
87
+
88
+ def evaluate_async_script(script, *args)
89
+ result = browser.evaluate_async(script, session_wait_time, *native_args(args))
90
+ unwrap_script_result(result)
91
+ end
92
+
93
+ def execute_script(script, *args)
94
+ browser.execute(script, *native_args(args))
95
+ nil
96
+ end
97
+
98
+ def within_frame(name, &block)
99
+ browser.within_frame(name, &block)
100
+ end
101
+
102
+ def switch_to_frame(locator)
103
+ browser.switch_to_frame(locator)
104
+ end
105
+
106
+ def current_window_handle
107
+ browser.window_handle
108
+ end
109
+
110
+ def window_handles
111
+ browser.window_handles
112
+ end
113
+
114
+ def close_window(handle)
115
+ browser.close_window(handle)
116
+ end
117
+
118
+ def open_new_window
119
+ browser.open_new_window
120
+ end
121
+
122
+ def switch_to_window(handle)
123
+ browser.switch_to_window(handle)
124
+ end
125
+
126
+ def within_window(name, &block)
127
+ browser.within_window(name, &block)
128
+ end
129
+
130
+ def no_such_window_error
131
+ NoSuchWindowError
132
+ end
133
+
134
+ def reset!
135
+ browser.reset
136
+ browser.url_blacklist = @options[:url_blacklist] if @options.key?(:url_blacklist)
137
+ browser.url_whitelist = @options[:url_whitelist] if @options.key?(:url_whitelist)
138
+ @started = false
139
+ end
140
+
141
+ def save_screenshot(path, options = {})
142
+ browser.render(path, options)
143
+ end
144
+ alias_method :render, :save_screenshot
145
+
146
+ def render_base64(format = :png, options = {})
147
+ browser.render_base64(format, options)
148
+ end
149
+
150
+ def paper_size=(size = {})
151
+ browser.set_paper_size(size)
152
+ end
153
+
154
+ def zoom_factor=(zoom_factor)
155
+ browser.set_zoom_factor(zoom_factor)
156
+ end
157
+
158
+ def resize(width, height)
159
+ browser.resize(width, height)
160
+ end
161
+ alias_method :resize_window, :resize
162
+
163
+ def resize_window_to(handle, width, height)
164
+ within_window(handle) do
165
+ resize(width, height)
166
+ end
167
+ end
168
+
169
+ def maximize_window(handle)
170
+ resize_window_to(handle, *screen_size)
171
+ end
172
+
173
+ def window_size(handle)
174
+ within_window(handle) do
175
+ evaluate_script("[window.innerWidth, window.innerHeight]")
176
+ end
177
+ end
178
+
179
+ def scroll_to(left, top)
180
+ browser.scroll_to(left, top)
181
+ end
182
+
183
+ def network_traffic(type = nil)
184
+ browser.network_traffic(type)
185
+ end
186
+
187
+ def clear_network_traffic
188
+ browser.clear_network_traffic
189
+ end
190
+
191
+ def set_proxy(ip, port, type = "http", user = nil, password = nil)
192
+ browser.set_proxy(ip, port, type, user, password)
193
+ end
194
+
195
+ def headers
196
+ browser.headers
197
+ end
198
+
199
+ def headers=(headers)
200
+ browser.headers=(headers)
201
+ end
202
+
203
+ def add_headers(headers)
204
+ browser.add_headers(headers)
205
+ end
206
+
207
+ def add_header(name, value, permanent: true)
208
+ browser.add_header({ name => value }, permanent: permanent)
209
+ end
210
+
211
+ def response_headers
212
+ browser.response_headers
213
+ end
214
+
215
+ def cookies
216
+ browser.cookies
217
+ end
218
+
219
+ def set_cookie(name, value, options = {})
220
+ options = options.dup
221
+ options[:name] ||= name
222
+ options[:value] ||= value
223
+ options[:domain] ||= default_domain
224
+
225
+ expires = options.delete(:expires).to_i
226
+ options[:expires] = expires if expires > 0
227
+
228
+ browser.set_cookie(options)
229
+ end
230
+
231
+ def remove_cookie(name, **options)
232
+ options[:domain] = default_domain if options.empty?
233
+ browser.remove_cookie(options.merge(name: name))
234
+ end
235
+
236
+ def clear_cookies
237
+ browser.clear_cookies
238
+ end
239
+
240
+ def clear_memory_cache
241
+ browser.clear_memory_cache
242
+ end
243
+
244
+ # * Browser with set settings does not send `Authorize` on POST request
245
+ # * With manually set header browser makes next request with
246
+ # `Authorization: Basic Og==` header when settings are empty and the
247
+ # response was `401 Unauthorized` (which means Base64.encode64(":")).
248
+ # Combining both methods to reach proper behavior.
249
+ def basic_authorize(user, password)
250
+ browser.set_http_auth(user, password)
251
+ credentials = ["#{user}:#{password}"].pack("m*").strip
252
+ add_header("Authorization", "Basic #{credentials}")
253
+ end
254
+
255
+ def pause
256
+ # STDIN is not necessarily connected to a keyboard. It might even be closed.
257
+ # So we need a method other than keypress to continue.
258
+
259
+ # In jRuby - STDIN returns immediately from select
260
+ # see https://github.com/jruby/jruby/issues/1783
261
+ read, write = IO.pipe
262
+ Thread.new { IO.copy_stream(STDIN, write); write.close }
263
+
264
+ STDERR.puts "Cuprite execution paused. Press enter (or run 'kill -CONT #{Process.pid}') to continue."
265
+
266
+ signal = false
267
+ old_trap = trap("SIGCONT") { signal = true; STDERR.puts "\nSignal SIGCONT received" }
268
+ keyboard = IO.select([read], nil, nil, 1) until keyboard || signal # wait for data on STDIN or signal SIGCONT received
269
+
270
+ unless signal
271
+ begin
272
+ input = read.read_nonblock(80) # clear out the read buffer
273
+ puts unless input&.end_with?("\n")
274
+ rescue EOFError, IO::WaitReadable # Ignore problems reading from STDIN.
275
+ end
276
+ end
277
+ ensure
278
+ trap("SIGCONT", old_trap) # Restore the previous signal handler, if there was one.
279
+ STDERR.puts "Continuing"
280
+ end
281
+
282
+ def wait?
283
+ true
284
+ end
285
+
286
+ def invalid_element_errors
287
+ [Capybara::Cuprite::ObsoleteNode, Capybara::Cuprite::MouseEventFailed]
288
+ end
289
+
290
+ def go_back
291
+ browser.go_back
292
+ end
293
+
294
+ def go_forward
295
+ browser.go_forward
296
+ end
297
+
298
+ def refresh
299
+ browser.refresh
300
+ end
301
+
302
+ def accept_modal(type, options = {})
303
+ case type
304
+ when :confirm
305
+ browser.accept_confirm
306
+ when :prompt
307
+ browser.accept_prompt options[:with]
308
+ end
309
+
310
+ yield if block_given?
311
+
312
+ find_modal(options)
313
+ end
314
+
315
+ def dismiss_modal(type, options = {})
316
+ case type
317
+ when :confirm
318
+ browser.dismiss_confirm
319
+ when :prompt
320
+ browser.dismiss_prompt
321
+ end
322
+
323
+ yield if block_given?
324
+ find_modal(options)
325
+ end
326
+
327
+ private
328
+
329
+ def default_domain
330
+ if @started
331
+ URI.parse(browser.current_url).host
332
+ else
333
+ URI.parse(default_cookie_host).host || "127.0.0.1"
334
+ end
335
+ end
336
+
337
+ def native_args(args)
338
+ args.map { |arg| arg.is_a?(Capybara::Cuprite::Node) ? arg.native : arg }
339
+ end
340
+
341
+ def screen_size
342
+ @options[:screen_size] || [1366, 768]
343
+ end
344
+
345
+ def find_modal(options)
346
+ start_time = Time.now
347
+ timeout_sec = options.fetch(:wait) { session_wait_time }
348
+ expect_text = options[:text]
349
+ expect_regexp = expect_text.is_a?(Regexp) ? expect_text : Regexp.escape(expect_text.to_s)
350
+ not_found_msg = "Unable to find modal dialog"
351
+ not_found_msg += " with #{expect_text}" if expect_text
352
+
353
+ begin
354
+ modal_text = browser.modal_message
355
+ raise Capybara::ModalNotFound if modal_text.nil? || (expect_text && !modal_text.match(expect_regexp))
356
+ rescue Capybara::ModalNotFound => e
357
+ raise e, not_found_msg if (Time.now - start_time) >= timeout_sec
358
+ sleep(0.05)
359
+ retry
360
+ end
361
+ modal_text
362
+ end
363
+
364
+ def session_wait_time
365
+ if respond_to?(:session_options)
366
+ session_options.default_max_wait_time
367
+ else
368
+ begin
369
+ Capybara.default_max_wait_time
370
+ rescue
371
+ Capybara.default_wait_time
372
+ end
373
+ end
374
+ end
375
+
376
+ def default_cookie_host
377
+ if respond_to?(:session_options)
378
+ session_options.app_host
379
+ else
380
+ Capybara.app_host
381
+ end || ""
382
+ end
383
+
384
+ def unwrap_script_result(arg)
385
+ case arg
386
+ when Array
387
+ arg.map { |e| unwrap_script_result(e) }
388
+ when Hash
389
+ return Capybara::Cuprite::Node.new(self, arg["target_id"], arg["node"]) if arg["target_id"]
390
+ arg.each { |k, v| arg[k] = unwrap_script_result(v) }
391
+ else
392
+ arg
393
+ end
394
+ end
395
+ end
396
+ end