apparition 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +251 -0
- data/lib/capybara/apparition.rb +20 -0
- data/lib/capybara/apparition/browser.rb +532 -0
- data/lib/capybara/apparition/chrome_client.rb +235 -0
- data/lib/capybara/apparition/command.rb +21 -0
- data/lib/capybara/apparition/cookie.rb +51 -0
- data/lib/capybara/apparition/dev_tools_protocol/session.rb +29 -0
- data/lib/capybara/apparition/dev_tools_protocol/target.rb +52 -0
- data/lib/capybara/apparition/dev_tools_protocol/target_manager.rb +37 -0
- data/lib/capybara/apparition/driver.rb +505 -0
- data/lib/capybara/apparition/errors.rb +230 -0
- data/lib/capybara/apparition/frame.rb +90 -0
- data/lib/capybara/apparition/frame_manager.rb +81 -0
- data/lib/capybara/apparition/inspector.rb +49 -0
- data/lib/capybara/apparition/keyboard.rb +383 -0
- data/lib/capybara/apparition/launcher.rb +218 -0
- data/lib/capybara/apparition/mouse.rb +47 -0
- data/lib/capybara/apparition/network_traffic.rb +9 -0
- data/lib/capybara/apparition/network_traffic/error.rb +12 -0
- data/lib/capybara/apparition/network_traffic/request.rb +47 -0
- data/lib/capybara/apparition/network_traffic/response.rb +49 -0
- data/lib/capybara/apparition/node.rb +844 -0
- data/lib/capybara/apparition/page.rb +711 -0
- data/lib/capybara/apparition/utility.rb +15 -0
- data/lib/capybara/apparition/version.rb +7 -0
- data/lib/capybara/apparition/web_socket_client.rb +80 -0
- metadata +245 -0
@@ -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
|