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