apparition 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/apparition/errors'
4
+ require 'capybara/apparition/web_socket_client'
5
+
6
+ module Capybara::Apparition
7
+ class ChromeClient
8
+ class << self
9
+ DEFAULT_OPTIONS = {
10
+ host: 'localhost',
11
+ port: 9222
12
+ }.freeze
13
+
14
+ def client(ws_url)
15
+ new(ws_url)
16
+ end
17
+
18
+ private
19
+
20
+ def get_ws_url(options)
21
+ response = Net::HTTP.get(options[:host], '/json', options[:port])
22
+ # TODO: handle unsuccesful request
23
+ response = JSON.parse(response)
24
+
25
+ first_page = response.find { |e| e['type'] == 'page' }
26
+ # TODO: handle no entry found
27
+ first_page['webSocketDebuggerUrl']
28
+ end
29
+ end
30
+
31
+ def initialize(ws_url)
32
+ @ws = WebSocketClient.new(ws_url)
33
+ @handlers = Hash.new { |hash, key| hash[key] = [] }
34
+
35
+ @responses = {}
36
+
37
+ @events = Queue.new
38
+
39
+ @send_mutex = Mutex.new
40
+ @msg_mutex = Mutex.new
41
+ @message_available = ConditionVariable.new
42
+ @session_handlers = Hash.new { |hash, key| hash[key] = Hash.new { |h, k| h[k] = [] } }
43
+ @timeout = nil
44
+ @async_ids = []
45
+
46
+ start_threads
47
+ end
48
+
49
+ attr_accessor :timeout
50
+
51
+ def stop
52
+ @ws.close
53
+ end
54
+
55
+ def on(event_name, session_id = nil, &block)
56
+ return @handlers[event_name] << block unless session_id
57
+
58
+ @session_handlers[session_id][event_name] << block
59
+ end
60
+
61
+ def send_cmd(command, params, async:)
62
+ msg_id, msg = generate_msg(command, params, async: async)
63
+ @send_mutex.synchronize do
64
+ puts "#{Time.now.to_i}: sending msg: #{msg}" if ENV['DEBUG']
65
+ @ws.send_msg(msg)
66
+ end
67
+
68
+ return nil if async
69
+
70
+ puts "waiting for session response for message #{msg_id}" if ENV['DEUG']
71
+ response = wait_for_msg_response(msg_id)
72
+
73
+ raise CDPError(response['error']) if response['error']
74
+
75
+ response['result']
76
+ end
77
+
78
+ def send_cmd_to_session(session_id, command, params, async:)
79
+ msg_id, msg = generate_msg(command, params, async: async)
80
+
81
+ send_cmd('Target.sendMessageToTarget', { sessionId: session_id, message: msg }, async: async)
82
+
83
+ return nil if async
84
+
85
+ puts "waiting for session response for message #{msg_id}" if ENV['DEBUG'] == 'V'
86
+ response = wait_for_msg_response(msg_id)
87
+
88
+ if (error = response['error'])
89
+ case error['code']
90
+ when -32_000
91
+ raise WrongWorld.new(nil, error)
92
+ else
93
+ raise CDPError.new(error)
94
+ end
95
+ end
96
+
97
+ response['result']
98
+ end
99
+
100
+ def listen_until
101
+ read_until { yield }
102
+ end
103
+
104
+ def listen
105
+ read_until { false }
106
+ end
107
+
108
+ private
109
+
110
+ def generate_msg(command, params, async:)
111
+ @send_mutex.synchronize do
112
+ msg_id = generate_unique_id
113
+ @async_ids.push(msg_id) if async
114
+ [msg_id, { method: command, params: params, id: msg_id }.to_json]
115
+ end
116
+ end
117
+
118
+ def wait_for_msg_response(msg_id)
119
+ @msg_mutex.synchronize do
120
+ start_time = Time.now
121
+ while (response = @responses.delete(msg_id)).nil?
122
+ if @timeout && ((Time.now - start_time) > @timeout)
123
+ puts "Timedout waiting for response for msg: #{msg_id}"
124
+ raise TimeoutError.new(msg_id)
125
+ end
126
+ @message_available.wait(@msg_mutex, 0.1)
127
+ end
128
+ response
129
+ end
130
+ end
131
+
132
+ def generate_unique_id
133
+ @last_id ||= 0
134
+ @last_id += 1
135
+ end
136
+
137
+ def read_until
138
+ loop do
139
+ msg = read_msg
140
+ return msg if yield(msg)
141
+ end
142
+ end
143
+
144
+ def read_msg
145
+ msg = JSON.parse(@ws.read_msg)
146
+ puts "#{Time.now.to_i}: got msg: #{msg}" if ENV['DEBUG']
147
+ # Check if it's an event and push on event queue
148
+ @events.push msg.dup if msg['method']
149
+
150
+ msg = JSON.parse(msg['params']['message']) if msg['method'] == 'Target.receivedMessageFromTarget'
151
+
152
+ if msg['id']
153
+ @msg_mutex.synchronize do
154
+ puts "broadcasting response to #{msg['id']}" if ENV['DEBUG'] == 'V'
155
+ @responses[msg['id']] = msg
156
+ @message_available.broadcast
157
+ end
158
+ end
159
+ msg
160
+ end
161
+
162
+ def cleanup_async_responses
163
+ loop do
164
+ @msg_mutex.synchronize do
165
+ @message_available.wait(@msg_mutex, 0.1)
166
+ (@responses.keys & @async_ids).each do |msg_id|
167
+ puts "Cleaning up response for #{msg_id}" if ENV['DEBUG'] == 'v'
168
+ @responses.delete(msg_id)
169
+ @async_ids.delete(msg_id)
170
+ end
171
+ end
172
+ end
173
+ end
174
+
175
+ def process_messages
176
+ # run handlers in own thread so as not to hang message processing
177
+ loop do
178
+ event = @events.pop
179
+ next unless event
180
+
181
+ event_name = event['method']
182
+ puts "popped event #{event_name}" if ENV['DEBUG'] == 'V'
183
+
184
+ if event_name == 'Target.receivedMessageFromTarget'
185
+ session_id = event.dig('params', 'sessionId')
186
+ event = JSON.parse(event.dig('params', 'message'))
187
+ event_name = event['method']
188
+ if event_name
189
+ puts "calling session handler for #{event_name}" if ENV['DEBUG'] == 'V'
190
+ @session_handlers[session_id][event_name].each do |handler|
191
+ handler.call(event['params'])
192
+ end
193
+ end
194
+ end
195
+
196
+ @handlers[event_name].each do |handler|
197
+ puts "calling handler for #{event_name}" if ENV['DEBUG'] == 'V'
198
+ handler.call(event['params'])
199
+ end
200
+ end
201
+ rescue CDPError => e
202
+ if e.code == -32_602
203
+ puts "Attempt to contact session that's gone away"
204
+ else
205
+ puts "Unexpected CDPError: #{e.message}"
206
+ end
207
+ retry
208
+ rescue StandardError => e
209
+ puts "Unexpected inner loop exception: #{e}: #{e.message}: #{e.backtrace}"
210
+ retry
211
+ rescue Exception => e # rubocop:disable Lint/RescueException
212
+ puts "Unexpected Outer Loop exception: #{e}"
213
+ retry
214
+ end
215
+
216
+ def start_threads
217
+ @processor = Thread.new do
218
+ process_messages
219
+ end
220
+ @processor.abort_on_exception = true
221
+
222
+ @async_response_handler = Thread.new do
223
+ cleanup_async_responses
224
+ end
225
+ @async_response_handler.abort_on_exception = true
226
+
227
+ @listener = Thread.new do
228
+ begin
229
+ listen
230
+ rescue EOFError # rubocop:disable Lint/HandleExceptions
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Capybara::Apparition
6
+ class Command
7
+ attr_reader :id
8
+ attr_reader :name
9
+ attr_accessor :args
10
+
11
+ def initialize(name, params = {})
12
+ @id = SecureRandom.uuid
13
+ @name = name
14
+ @params = params
15
+ end
16
+
17
+ def message
18
+ JSON.dump('id' => @id, 'name' => @name, 'params' => @params)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara::Apparition
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 secure?
26
+ @attributes['secure']
27
+ end
28
+
29
+ def http_only?
30
+ @attributes['httpOnly']
31
+ end
32
+ alias httponly? http_only?
33
+
34
+ def httpOnly? # rubocop:disable Naming/MethodName
35
+ warn 'httpOnly? is deprecated, please use http_only? instead'
36
+ http_only?
37
+ end
38
+
39
+ def same_site
40
+ @attributes['sameSite']
41
+ end
42
+
43
+ def samesite
44
+ same_site
45
+ end
46
+
47
+ def expires
48
+ Time.at @attributes['expires'] unless [nil, 0, -1].include? @attributes['expires']
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara::Apparition
4
+ module DevToolsProtocol
5
+ class Session
6
+ attr_reader :browser, :connection, :target_id, :session_id
7
+
8
+ def initialize(browser, connection, target_id, session_id)
9
+ @browser = browser
10
+ @connection = connection
11
+ @target_id = target_id
12
+ @session_id = session_id
13
+ @handlers = []
14
+ end
15
+
16
+ def command(name, **params)
17
+ @browser.command_for_session(@session_id, name, params, async: false)
18
+ end
19
+
20
+ def async_command(name, **params)
21
+ @browser.command_for_session(@session_id, name, params, async: true)
22
+ end
23
+
24
+ def on(event_name, &block)
25
+ connection.on(event_name, @session_id, &block)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/apparition/dev_tools_protocol/session'
4
+
5
+ module Capybara::Apparition
6
+ module DevToolsProtocol
7
+ class Target
8
+ attr_accessor :info
9
+
10
+ def initialize(browser, info)
11
+ @browser = browser
12
+ @info = info
13
+ @page = nil
14
+ end
15
+
16
+ def id
17
+ info['targetId']
18
+ end
19
+
20
+ def title
21
+ info['title']
22
+ end
23
+
24
+ def url
25
+ info['url']
26
+ end
27
+
28
+ def page
29
+ @page ||= begin
30
+ if info['type'] == 'page'
31
+ Page.create(@browser, create_session, id,
32
+ ignore_https_errors: true,
33
+ js_errors: @browser.js_errors).inherit(info.delete('inherit'))
34
+ else
35
+ nil
36
+ end
37
+ end
38
+ end
39
+
40
+ def close
41
+ @browser.command('Target.closeTarget', targetId: id)
42
+ end
43
+
44
+ private
45
+
46
+ def create_session
47
+ session_id = @browser.command('Target.attachToTarget', targetId: id)['sessionId']
48
+ Session.new(@browser, @browser.client, id, session_id)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/apparition/dev_tools_protocol/target'
4
+
5
+ module Capybara::Apparition
6
+ module DevToolsProtocol
7
+ class TargetManager
8
+ def initialize
9
+ @targets = {}
10
+ end
11
+
12
+ def get(id)
13
+ @targets[id]
14
+ end
15
+
16
+ def add(id, target)
17
+ @targets[id] = target
18
+ end
19
+
20
+ def delete(id)
21
+ @targets.delete(id)
22
+ end
23
+
24
+ def pages
25
+ @targets.values.select { |target| target.info['type'] == 'page' }.map(&:page)
26
+ end
27
+
28
+ def target?(id)
29
+ @targets.key?(id)
30
+ end
31
+
32
+ def window_handles
33
+ @targets.values.select { |target| target.info['type'] == 'page' }.map(&:id).compact
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,505 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'capybara/apparition/chrome_client'
5
+ require 'capybara/apparition/launcher'
6
+
7
+ module Capybara::Apparition
8
+ class Driver < Capybara::Driver::Base
9
+ DEFAULT_TIMEOUT = 30
10
+
11
+ attr_reader :app, :options
12
+
13
+ def initialize(app, options = {})
14
+ @app = app
15
+ @options = options
16
+ @browser = nil
17
+ @inspector = nil
18
+ @client = nil
19
+ @started = false
20
+ end
21
+
22
+ def needs_server?
23
+ true
24
+ end
25
+
26
+ def chrome_url
27
+ 'ws://localhost:9223'
28
+ end
29
+
30
+ def browser
31
+ @browser ||= begin
32
+ browser = Browser.new(client, browser_logger)
33
+ browser.js_errors = options[:js_errors] if options.key?(:js_errors)
34
+ browser.extensions = options.fetch(:extensions, [])
35
+ browser.debug = true if options[:debug]
36
+ browser.url_blacklist = options[:url_blacklist] || []
37
+ browser.url_whitelist = options[:url_whitelist] || []
38
+ browser
39
+ end
40
+ end
41
+
42
+ def inspector
43
+ @inspector ||= options[:inspector] && Inspector.new(options[:inspector])
44
+ end
45
+
46
+ def client
47
+ @client ||= begin
48
+ browser_options = {}
49
+ browser_options['remote-debugging-port'] = options[:port] || 0
50
+ browser_options['remote-debugging-address'] = options[:host] if options[:host]
51
+ browser_options['window-size'] = options[:window_size].join(',') if options[:window_size]
52
+ @launcher ||= Browser::Launcher.start(
53
+ headless: options[:headless] != false,
54
+ browser: browser_options
55
+ )
56
+ ws_url = @launcher.ws_url
57
+ client = ::Capybara::Apparition::ChromeClient.client(ws_url.to_s)
58
+ sleep 3
59
+ client
60
+ end
61
+ end
62
+
63
+ def browser_options
64
+ list = options[:browser_options] || []
65
+
66
+ # TODO: configure SSL options
67
+ # PhantomJS defaults to only using SSLv3, which since POODLE (Oct 2014)
68
+ # many sites have dropped from their supported protocols (eg PayPal,
69
+ # Braintree).
70
+ # list += ["--ignore-ssl-errors=yes"] unless list.grep(/ignore-ssl-errors/).any?
71
+ # list += ["--ssl-protocol=TLSv1"] unless list.grep(/ssl-protocol/).any?
72
+ # list += ["--remote-debugger-port=#{inspector.port}", "--remote-debugger-autorun=yes"] if inspector
73
+ list
74
+ end
75
+
76
+ def restart
77
+ browser.restart
78
+ end
79
+
80
+ def quit
81
+ @client&.stop
82
+ @launcher&.stop
83
+ end
84
+
85
+ # logger should be an object that responds to puts, or nil
86
+ def logger
87
+ options[:logger] || (options[:debug] && STDERR)
88
+ end
89
+
90
+ # logger should be an object that behaves like IO or nil
91
+ def browser_logger
92
+ options.fetch(:browser_logger, nil)
93
+ end
94
+
95
+ def visit(url)
96
+ @started = true
97
+ browser.visit(url)
98
+ end
99
+
100
+ def current_url
101
+ browser.current_url
102
+ end
103
+
104
+ def status_code
105
+ browser.status_code
106
+ end
107
+
108
+ def html
109
+ browser.body
110
+ end
111
+ alias body html
112
+
113
+ def source
114
+ browser.source.to_s
115
+ end
116
+
117
+ def title
118
+ browser.title
119
+ end
120
+
121
+ def frame_title
122
+ browser.frame_title
123
+ end
124
+
125
+ def frame_url
126
+ browser.frame_url
127
+ end
128
+
129
+ def find(method, selector)
130
+ browser.find(method, selector).map { |page_id, id| Capybara::Apparition::Node.new(self, page_id, id) }
131
+ end
132
+
133
+ def find_xpath(selector)
134
+ find :xpath, selector.to_s
135
+ end
136
+
137
+ def find_css(selector)
138
+ find :css, selector.to_s
139
+ end
140
+
141
+ def click(x, y)
142
+ browser.click_coordinates(x, y)
143
+ end
144
+
145
+ def evaluate_script(script, *args)
146
+ result = browser.evaluate(script, *native_args(args))
147
+ unwrap_script_result(result)
148
+ end
149
+
150
+ def evaluate_async_script(script, *args)
151
+ result = browser.evaluate_async(script, session_wait_time, *native_args(args))
152
+ unwrap_script_result(result)
153
+ end
154
+
155
+ def execute_script(script, *args)
156
+ browser.execute(script, *native_args(args))
157
+ nil
158
+ end
159
+
160
+ def switch_to_frame(frame)
161
+ browser.switch_to_frame(frame)
162
+ end
163
+
164
+ def current_window_handle
165
+ browser.window_handle
166
+ end
167
+
168
+ def window_handles
169
+ browser.window_handles
170
+ end
171
+
172
+ def close_window(handle)
173
+ browser.close_window(handle)
174
+ end
175
+
176
+ def open_new_window
177
+ browser.open_new_window
178
+ end
179
+
180
+ def switch_to_window(handle)
181
+ browser.switch_to_window(handle)
182
+ end
183
+
184
+ def within_window(name, &block)
185
+ browser.within_window(name, &block)
186
+ end
187
+
188
+ def no_such_window_error
189
+ NoSuchWindowError
190
+ end
191
+
192
+ def reset!
193
+ browser.reset
194
+ # TODO: reset the black/whitelists
195
+ # browser.url_blacklist = options[:url_blacklist] || []
196
+ # browser.url_whitelist = options[:url_whitelist] || []
197
+ @started = false
198
+ end
199
+
200
+ def save_screenshot(path, options = {})
201
+ browser.render(path, options)
202
+ end
203
+ alias render save_screenshot
204
+
205
+ def render_base64(format = :png, options = {})
206
+ browser.render_base64(format, options)
207
+ end
208
+
209
+ def paper_size=(size = {})
210
+ browser.set_paper_size(size)
211
+ end
212
+
213
+ # def zoom_factor=(zoom_factor)
214
+ # TODO: Implement if still necessary
215
+ # browser.set_zoom_factor(zoom_factor)
216
+ # end
217
+
218
+ def resize(width, height)
219
+ browser.resize(width, height, screen: options[:screen_size])
220
+ end
221
+ alias resize_window resize
222
+
223
+ def resize_window_to(handle, width, height)
224
+ within_window(handle) do
225
+ resize(width, height)
226
+ end
227
+ end
228
+
229
+ def maximize_window(handle)
230
+ within_window(handle) do
231
+ browser.maximize
232
+ end
233
+ end
234
+
235
+ def fullscreen_window(handle)
236
+ within_window(handle) do
237
+ browser.fullscreen
238
+ end
239
+ end
240
+
241
+ def window_size(handle)
242
+ within_window(handle) do
243
+ evaluate_script('[window.innerWidth, window.innerHeight]')
244
+ end
245
+ end
246
+
247
+ def scroll_to(left, top)
248
+ browser.scroll_to(left, top)
249
+ end
250
+
251
+ def network_traffic(type = nil)
252
+ browser.network_traffic(type)
253
+ end
254
+
255
+ def clear_network_traffic
256
+ browser.clear_network_traffic
257
+ end
258
+
259
+ def set_proxy(ip, port, type = 'http', user = nil, password = nil)
260
+ browser.set_proxy(ip, port, type, user, password)
261
+ end
262
+
263
+ def headers
264
+ browser.get_headers
265
+ end
266
+
267
+ def headers=(headers)
268
+ browser.set_headers(headers)
269
+ end
270
+
271
+ def add_headers(headers)
272
+ browser.add_headers(headers)
273
+ end
274
+
275
+ def add_header(name, value, options = {})
276
+ browser.add_header({ name => value }, { permanent: true }.merge(options))
277
+ end
278
+
279
+ def response_headers
280
+ browser.response_headers.each_with_object({}) do |(key, value), hsh|
281
+ hsh[key.split('-').map(&:capitalize).join('-')] = value
282
+ end
283
+ end
284
+
285
+ def cookies
286
+ browser.cookies
287
+ end
288
+
289
+ def set_cookie(name, value, options = {})
290
+ options[:name] ||= name
291
+ options[:value] ||= value
292
+ options[:domain] ||= begin
293
+ if @started
294
+ URI.parse(browser.current_url).host
295
+ else
296
+ URI.parse(default_cookie_host).host || '127.0.0.1'
297
+ end
298
+ end
299
+
300
+ browser.set_cookie(options)
301
+ end
302
+
303
+ def remove_cookie(name)
304
+ browser.remove_cookie(name)
305
+ end
306
+
307
+ def clear_cookies
308
+ browser.clear_cookies
309
+ end
310
+
311
+ def cookies_enabled=(flag)
312
+ browser.cookies_enabled = flag
313
+ end
314
+
315
+ def clear_memory_cache
316
+ browser.clear_memory_cache
317
+ end
318
+
319
+ def basic_authorize(user = nil, password = nil)
320
+ browser.set_http_auth(user, password)
321
+ # credentials = ["#{user}:#{password}"].pack('m*').strip
322
+ # add_header('Authorization', "Basic #{credentials}")
323
+ end
324
+
325
+ def debug
326
+ if @options[:inspector]
327
+ # Fall back to default scheme
328
+ scheme = begin
329
+ URI.parse(browser.current_url).scheme
330
+ rescue StandardError
331
+ nil
332
+ end
333
+ scheme = 'http' if scheme != 'https'
334
+ inspector.open(scheme)
335
+ pause
336
+ else
337
+ raise Error, 'To use the remote debugging, you have to launch the driver ' \
338
+ 'with `:inspector => true` configuration option'
339
+ end
340
+ end
341
+
342
+ def pause
343
+ # STDIN is not necessarily connected to a keyboard. It might even be closed.
344
+ # So we need a method other than keypress to continue.
345
+
346
+ # In jRuby - STDIN returns immediately from select
347
+ # see https://github.com/jruby/jruby/issues/1783
348
+ # TODO: This limitation is no longer true can we simplify?
349
+ read, write = IO.pipe
350
+ Thread.new do
351
+ IO.copy_stream(STDIN, write)
352
+ write.close
353
+ end
354
+
355
+ STDERR.puts "Apparition execution paused. Press enter (or run 'kill -CONT #{Process.pid}') to continue."
356
+
357
+ signal = false
358
+ old_trap = trap('SIGCONT') do
359
+ signal = true
360
+ STDERR.puts "\nSignal SIGCONT received"
361
+ end
362
+ # wait for data on STDIN or signal SIGCONT received
363
+ keyboard = IO.select([read], nil, nil, 1) until keyboard || signal
364
+
365
+ unless signal
366
+ begin
367
+ input = read.read_nonblock(80) # clear out the read buffer
368
+ puts unless input&.end_with?("\n")
369
+ rescue EOFError, IO::WaitReadable # rubocop:disable Lint/HandleExceptions
370
+ # Ignore problems reading from STDIN.
371
+ end
372
+ end
373
+ ensure
374
+ trap('SIGCONT', old_trap) # Restore the previous signal handler, if there was one.
375
+ STDERR.puts 'Continuing'
376
+ end
377
+
378
+ def wait?
379
+ true
380
+ end
381
+
382
+ def invalid_element_errors
383
+ [Capybara::Apparition::ObsoleteNode, Capybara::Apparition::MouseEventFailed]
384
+ end
385
+
386
+ def go_back
387
+ browser.go_back
388
+ end
389
+
390
+ def go_forward
391
+ browser.go_forward
392
+ end
393
+
394
+ def refresh
395
+ browser.refresh
396
+ end
397
+
398
+ def accept_modal(type, options = {})
399
+ case type
400
+ when :alert
401
+ browser.accept_alert
402
+ when :confirm
403
+ browser.accept_confirm
404
+ when :prompt
405
+ browser.accept_prompt options[:with]
406
+ end
407
+
408
+ yield if block_given?
409
+
410
+ find_modal(options)
411
+ end
412
+
413
+ def dismiss_modal(type, options = {})
414
+ case type
415
+ when :confirm
416
+ browser.dismiss_confirm
417
+ when :prompt
418
+ browser.dismiss_prompt
419
+ end
420
+
421
+ yield if block_given?
422
+ find_modal(options)
423
+ end
424
+
425
+ def timeout
426
+ client.timeout
427
+ end
428
+
429
+ def timeout=(sec)
430
+ client.timeout = sec
431
+ end
432
+
433
+ private
434
+
435
+ def screen_size
436
+ options[:screen_size] || [1366, 768]
437
+ end
438
+
439
+ def find_modal(options)
440
+ start_time = Time.now
441
+ timeout_sec = options.fetch(:wait) { session_wait_time }
442
+ expect_text = options[:text]
443
+ expect_regexp = expect_text.is_a?(Regexp) ? expect_text : Regexp.escape(expect_text.to_s)
444
+ begin
445
+ modal_text = browser.modal_message
446
+ found_text ||= modal_text
447
+ raise Capybara::ModalNotFound if modal_text.nil? || (expect_text && !modal_text.match(expect_regexp))
448
+ rescue Capybara::ModalNotFound => e
449
+ if (Time.now - start_time) >= timeout_sec
450
+ raise e, 'Unable to find modal dialog'\
451
+ "#{" with #{expect_text}" if expect_text}"\
452
+ "#{", did find modal with #{found_text}" if found_text}"
453
+ end
454
+ sleep(0.5)
455
+ retry
456
+ end
457
+ modal_text
458
+ end
459
+
460
+ def session_wait_time
461
+ if respond_to?(:session_options)
462
+ session_options.default_max_wait_time
463
+ else
464
+ begin begin
465
+ Capybara.default_max_wait_time
466
+ rescue StandardError
467
+ Capybara.default_wait_time
468
+ end end
469
+ end
470
+ end
471
+
472
+ def default_cookie_host
473
+ if respond_to?(:session_options)
474
+ session_options.app_host
475
+ else
476
+ Capybara.app_host
477
+ end || ''
478
+ end
479
+
480
+ def native_args(args)
481
+ args.map { |arg| arg.is_a?(Capybara::Apparition::Node) ? arg.native : arg }
482
+ end
483
+
484
+ def unwrap_script_result(arg, object_cache = {})
485
+ return object_cache[arg] if object_cache.key? arg
486
+
487
+ case arg
488
+ when Array
489
+ object_cache[arg] = []
490
+ object_cache[arg].replace(arg.map { |e| unwrap_script_result(e, object_cache) })
491
+ object_cache[arg]
492
+ when Hash
493
+ if (arg['subtype'] == 'node') && arg['objectId']
494
+ Capybara::Apparition::Node.new(self, browser.current_page, arg['objectId'])
495
+ else
496
+ object_cache[arg] = {}
497
+ arg.each { |k, v| object_cache[arg][k] = unwrap_script_result(v, object_cache) }
498
+ object_cache[arg]
499
+ end
500
+ else
501
+ arg
502
+ end
503
+ end
504
+ end
505
+ end