apparition 0.0.1

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