terminus 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +25 -19
- data/bin/terminus +1 -1
- data/lib/capybara/driver/terminus.rb +28 -17
- data/lib/terminus.rb +29 -15
- data/lib/terminus/application.rb +5 -11
- data/lib/terminus/browser.rb +97 -44
- data/lib/terminus/client.rb +43 -0
- data/lib/terminus/client/browser.rb +30 -0
- data/lib/terminus/client/phantom.js +6 -0
- data/lib/terminus/client/phantomjs.rb +20 -0
- data/lib/terminus/connector.rb +9 -0
- data/lib/terminus/connector/server.rb +142 -0
- data/lib/terminus/connector/socket_handler.rb +72 -0
- data/lib/terminus/controller.rb +22 -5
- data/lib/terminus/host.rb +7 -2
- data/lib/terminus/node.rb +11 -5
- data/lib/terminus/proxy.rb +35 -1
- data/lib/terminus/proxy/driver_body.rb +26 -6
- data/lib/terminus/proxy/external.rb +9 -0
- data/lib/terminus/proxy/rewrite.rb +4 -2
- data/lib/terminus/public/compiled/terminus-min.js +3 -0
- data/lib/terminus/public/compiled/terminus.js +5270 -0
- data/lib/terminus/public/loader.js +1 -1
- data/lib/terminus/public/pathology.js +3174 -0
- data/lib/terminus/public/syn/browsers.js +2 -2
- data/lib/terminus/public/syn/drag/drag.js +3 -4
- data/lib/terminus/public/syn/key.js +130 -111
- data/lib/terminus/public/syn/mouse.js +2 -2
- data/lib/terminus/public/syn/synthetic.js +45 -34
- data/lib/terminus/public/terminus.js +183 -70
- data/lib/terminus/server.rb +6 -0
- data/lib/terminus/timeouts.rb +4 -2
- data/lib/terminus/views/bootstrap.erb +12 -27
- data/lib/terminus/views/index.erb +1 -1
- data/spec/reports/android.txt +875 -0
- data/spec/reports/chrome.txt +137 -8
- data/spec/reports/firefox.txt +137 -9
- data/spec/reports/opera.txt +142 -13
- data/spec/reports/phantomjs.txt +871 -0
- data/spec/reports/safari.txt +137 -8
- data/spec/spec_helper.rb +19 -17
- data/spec/terminus_driver_spec.rb +8 -6
- data/spec/terminus_session_spec.rb +4 -4
- metadata +209 -117
@@ -0,0 +1,43 @@
|
|
1
|
+
module Terminus
|
2
|
+
module Client
|
3
|
+
|
4
|
+
autoload :Browser, ROOT + '/terminus/client/browser'
|
5
|
+
autoload :PhantomJS, ROOT + '/terminus/client/phantomjs'
|
6
|
+
|
7
|
+
class Base
|
8
|
+
def self.start(options = {})
|
9
|
+
process = new(options)
|
10
|
+
process.start
|
11
|
+
process
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(options)
|
15
|
+
@address = TCPServer.new(0).addr
|
16
|
+
@port = options[:port] || @address[1]
|
17
|
+
@terminus = Terminus.create(:port => @port)
|
18
|
+
@browser = ChildProcess.build(*browser_args(options[:command]))
|
19
|
+
end
|
20
|
+
|
21
|
+
def start
|
22
|
+
Terminus.ensure_reactor_running
|
23
|
+
@terminus.run!
|
24
|
+
|
25
|
+
@browser.start
|
26
|
+
|
27
|
+
Terminus.port = @port
|
28
|
+
Terminus.browser = browser_selector
|
29
|
+
|
30
|
+
at_exit { stop }
|
31
|
+
end
|
32
|
+
|
33
|
+
def stop
|
34
|
+
@terminus.stop!
|
35
|
+
@browser.stop
|
36
|
+
@browser.poll_for_exit(10)
|
37
|
+
rescue ChildProcess::TimeoutError
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Terminus
|
2
|
+
module Client
|
3
|
+
|
4
|
+
class Browser < Base
|
5
|
+
DEFAULT_COMMANDS = {
|
6
|
+
/(mingw|mswin|windows|cygwin)/i => ['cmd', '/C', 'start', '/b'],
|
7
|
+
/(darwin|mac os)/i => ['open'],
|
8
|
+
/(linux|bsd|aix|solaris)/i => ['xdg-open']
|
9
|
+
}
|
10
|
+
|
11
|
+
def browser_args(command)
|
12
|
+
return command + [dock_url] if command
|
13
|
+
|
14
|
+
os = RbConfig::CONFIG['host_os']
|
15
|
+
key = DEFAULT_COMMANDS.keys.find { |key| os =~ key }
|
16
|
+
DEFAULT_COMMANDS[key] + [dock_url]
|
17
|
+
end
|
18
|
+
|
19
|
+
def browser_selector
|
20
|
+
{:current_url => dock_url}
|
21
|
+
end
|
22
|
+
|
23
|
+
def dock_url
|
24
|
+
"http://#{@address[2]}:#{@port}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Terminus
|
2
|
+
module Client
|
3
|
+
|
4
|
+
class PhantomJS < Base
|
5
|
+
DEFAULT_COMMAND = ['/usr/bin/env', 'phantomjs']
|
6
|
+
PHANTOM_CLIENT = File.expand_path('../phantom.js', __FILE__)
|
7
|
+
|
8
|
+
def browser_args(command)
|
9
|
+
args = (command || DEFAULT_COMMAND).dup
|
10
|
+
args + [PHANTOM_CLIENT, @address[2], @port.to_s]
|
11
|
+
end
|
12
|
+
|
13
|
+
def browser_selector
|
14
|
+
{:name => 'PhantomJS'}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,142 @@
|
|
1
|
+
# Based on code from the Poltergeist project
|
2
|
+
# https://github.com/jonleighton/poltergeist
|
3
|
+
#
|
4
|
+
# Copyright (c) 2011 Jonathan Leighton
|
5
|
+
#
|
6
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
7
|
+
# of this software and associated documentation files (the "Software"), to deal
|
8
|
+
# in the Software without restriction, including without limitation the rights
|
9
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
+
# copies of the Software, and to permit persons to whom the Software is
|
11
|
+
# furnished to do so, subject to the following conditions:
|
12
|
+
#
|
13
|
+
# The above copyright notice and this permission notice shall be included in
|
14
|
+
# all copies or substantial portions of the Software.
|
15
|
+
#
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
22
|
+
# SOFTWARE.
|
23
|
+
|
24
|
+
module Terminus
|
25
|
+
module Connector
|
26
|
+
|
27
|
+
class Server
|
28
|
+
RECV_SIZE = 1024
|
29
|
+
BIND_TIMEOUT = 5
|
30
|
+
|
31
|
+
def initialize(browser, timeout = BIND_TIMEOUT)
|
32
|
+
@browser = browser
|
33
|
+
@skips = 0
|
34
|
+
@server = start_server
|
35
|
+
@timeout = timeout
|
36
|
+
reset
|
37
|
+
end
|
38
|
+
|
39
|
+
def reset
|
40
|
+
@closing = false
|
41
|
+
@env = nil
|
42
|
+
@handler = nil
|
43
|
+
@parser = Http::Parser.new
|
44
|
+
@socket = nil
|
45
|
+
end
|
46
|
+
|
47
|
+
def connected?
|
48
|
+
not @socket.nil?
|
49
|
+
end
|
50
|
+
|
51
|
+
def port
|
52
|
+
@server.addr[1]
|
53
|
+
end
|
54
|
+
|
55
|
+
def request(message)
|
56
|
+
@browser.debug(:send, @browser.id, message)
|
57
|
+
accept unless connected?
|
58
|
+
@socket.write(@handler.encode(message))
|
59
|
+
true while @closing && receive
|
60
|
+
result = receive
|
61
|
+
@browser.debug(:recv, @browser.id, result)
|
62
|
+
reset if result.nil?
|
63
|
+
result
|
64
|
+
rescue Errno::ECONNRESET, Errno::EPIPE, Errno::EWOULDBLOCK
|
65
|
+
reset
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
|
69
|
+
def drain_socket
|
70
|
+
@closing = true if @socket
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def start_server
|
76
|
+
time = Time.now
|
77
|
+
TCPServer.open(0)
|
78
|
+
rescue Errno::EADDRINUSE
|
79
|
+
if (Time.now - time) < BIND_TIMEOUT
|
80
|
+
sleep(0.01)
|
81
|
+
retry
|
82
|
+
else
|
83
|
+
raise
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def accept
|
88
|
+
@skips.times { @server.accept.close }
|
89
|
+
@socket = @server.accept
|
90
|
+
while line = @socket.gets
|
91
|
+
@parser << line
|
92
|
+
break if line == "\r\n"
|
93
|
+
end
|
94
|
+
if line.nil?
|
95
|
+
accept
|
96
|
+
@skips += 1
|
97
|
+
else
|
98
|
+
@handler = SocketHandler.new(self, env)
|
99
|
+
@socket.write(@handler.handshake_response)
|
100
|
+
@browser.debug(:accept, @browser.id, @handler.url)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def env
|
105
|
+
@env ||= begin
|
106
|
+
env = {'REQUEST_METHOD' => @parser.http_method}
|
107
|
+
@parser.headers.each do |header, value|
|
108
|
+
env['HTTP_' + header.upcase.gsub('-', '_')] = value
|
109
|
+
end
|
110
|
+
if env['HTTP_SEC_WEBSOCKET_KEY1']
|
111
|
+
env['rack.input'] = StringIO.new(@socket.read(8))
|
112
|
+
end
|
113
|
+
env
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def receive
|
118
|
+
@browser.debug(:receive, @browser.id)
|
119
|
+
start = Time.now
|
120
|
+
|
121
|
+
until @handler.message?
|
122
|
+
raise Errno::EWOULDBLOCK if (Time.now - start) >= @timeout
|
123
|
+
IO.select([@socket], [], [], @timeout) or raise Errno::EWOULDBLOCK
|
124
|
+
data = @socket.recv(RECV_SIZE)
|
125
|
+
break if data.empty?
|
126
|
+
@handler << data
|
127
|
+
break if @handler.nil?
|
128
|
+
end
|
129
|
+
@handler && @handler.next_message
|
130
|
+
end
|
131
|
+
|
132
|
+
def close
|
133
|
+
[server, socket].compact.each do |s|
|
134
|
+
s.close_read
|
135
|
+
s.close_write
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# Based on code from the Poltergeist project
|
2
|
+
# https://github.com/jonleighton/poltergeist
|
3
|
+
#
|
4
|
+
# Copyright (c) 2011 Jonathan Leighton
|
5
|
+
#
|
6
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
7
|
+
# of this software and associated documentation files (the "Software"), to deal
|
8
|
+
# in the Software without restriction, including without limitation the rights
|
9
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
+
# copies of the Software, and to permit persons to whom the Software is
|
11
|
+
# furnished to do so, subject to the following conditions:
|
12
|
+
#
|
13
|
+
# The above copyright notice and this permission notice shall be included in
|
14
|
+
# all copies or substantial portions of the Software.
|
15
|
+
#
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
22
|
+
# SOFTWARE.
|
23
|
+
|
24
|
+
module Terminus
|
25
|
+
module Connector
|
26
|
+
|
27
|
+
class SocketHandler
|
28
|
+
attr_reader :env
|
29
|
+
|
30
|
+
def initialize(server, env)
|
31
|
+
@server = server
|
32
|
+
@env = env
|
33
|
+
@parser = Faye::WebSocket.parser(env).new(self)
|
34
|
+
@messages = []
|
35
|
+
end
|
36
|
+
|
37
|
+
def url
|
38
|
+
"ws://#{env['HTTP_HOST']}/"
|
39
|
+
end
|
40
|
+
|
41
|
+
def handshake_response
|
42
|
+
@parser.handshake_response
|
43
|
+
end
|
44
|
+
|
45
|
+
def <<(data)
|
46
|
+
@parser.parse(data)
|
47
|
+
end
|
48
|
+
|
49
|
+
def encode(message)
|
50
|
+
@parser.frame(Faye::WebSocket.encode(message))
|
51
|
+
end
|
52
|
+
|
53
|
+
def receive(message)
|
54
|
+
@messages << message
|
55
|
+
end
|
56
|
+
|
57
|
+
def message?
|
58
|
+
@messages.any?
|
59
|
+
end
|
60
|
+
|
61
|
+
def next_message
|
62
|
+
@messages.shift
|
63
|
+
end
|
64
|
+
|
65
|
+
def close(*args)
|
66
|
+
@server.reset
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
data/lib/terminus/controller.rb
CHANGED
@@ -11,7 +11,7 @@ module Terminus
|
|
11
11
|
|
12
12
|
def browser(id = nil)
|
13
13
|
return @browser if id.nil?
|
14
|
-
@browsers[id] ||= Browser.new(self)
|
14
|
+
@browsers[id] ||= Browser.new(self, id)
|
15
15
|
end
|
16
16
|
|
17
17
|
def browsers
|
@@ -26,6 +26,10 @@ module Terminus
|
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
|
+
def cookies
|
30
|
+
@cookies ||= CookieJar::Jar.new
|
31
|
+
end
|
32
|
+
|
29
33
|
def drop_browser(browser)
|
30
34
|
@browsers.delete(browser.id)
|
31
35
|
@browser = nil if @browser == browser
|
@@ -62,8 +66,9 @@ module Terminus
|
|
62
66
|
@host_aliases.index(Host.new(uri))
|
63
67
|
|
64
68
|
if remote_host
|
65
|
-
uri.
|
66
|
-
uri.
|
69
|
+
uri.scheme = remote_host.scheme
|
70
|
+
uri.host = remote_host.host
|
71
|
+
uri.port = remote_host.port
|
67
72
|
end
|
68
73
|
uri.host = dock_host if dock_host and uri.host =~ LOCALHOST
|
69
74
|
uri
|
@@ -73,8 +78,20 @@ module Terminus
|
|
73
78
|
uri = URI.parse(url)
|
74
79
|
return uri unless URI::HTTP === uri and uri.host !~ LOCALHOST and uri.host != dock_host
|
75
80
|
server = boot(uri)
|
76
|
-
uri.
|
81
|
+
uri.scheme = 'http'
|
82
|
+
uri.host, uri.port = (dock_host || server.host), server.port
|
77
83
|
uri
|
84
|
+
rescue URI::InvalidURIError
|
85
|
+
url
|
86
|
+
end
|
87
|
+
|
88
|
+
def server_running?(server)
|
89
|
+
return false unless server.port
|
90
|
+
uri = URI.parse("http://#{server.host}:#{server.port}/")
|
91
|
+
Net::HTTP.start(uri.host, uri.port) { |h| h.head(uri.path) }
|
92
|
+
true
|
93
|
+
rescue
|
94
|
+
false
|
78
95
|
end
|
79
96
|
|
80
97
|
private
|
@@ -93,7 +110,7 @@ module Terminus
|
|
93
110
|
@host_aliases[host] ||= begin
|
94
111
|
server = Capybara::Server.new(Proxy[host])
|
95
112
|
Thread.new { server.boot }
|
96
|
-
sleep
|
113
|
+
sleep(0.1) until server_running?(server)
|
97
114
|
Host.new(server)
|
98
115
|
end
|
99
116
|
end
|
data/lib/terminus/host.rb
CHANGED
@@ -4,8 +4,13 @@ module Terminus
|
|
4
4
|
attr_reader :host, :port
|
5
5
|
|
6
6
|
def initialize(uri)
|
7
|
-
@
|
8
|
-
@
|
7
|
+
@scheme = uri.scheme if uri.respond_to?(:scheme)
|
8
|
+
@host = uri.host
|
9
|
+
@port = uri.port
|
10
|
+
end
|
11
|
+
|
12
|
+
def scheme
|
13
|
+
@scheme || 'http'
|
9
14
|
end
|
10
15
|
|
11
16
|
def eql?(other)
|
data/lib/terminus/node.rb
CHANGED
@@ -18,14 +18,20 @@ module Terminus
|
|
18
18
|
def click
|
19
19
|
page = @browser.page_id
|
20
20
|
options = @driver ? @driver.options : {}
|
21
|
-
command = @browser.tell([:click, @id, options])
|
22
21
|
|
23
|
-
|
24
|
-
@browser.
|
22
|
+
value = if @browser.connector
|
23
|
+
@browser.ask([:click, @id, options], false)
|
24
|
+
else
|
25
|
+
command = @browser.tell([:click, @id, options])
|
26
|
+
|
27
|
+
result = @browser.wait_with_timeout(:click_response) do
|
28
|
+
@browser.result(command) || (@browser.page_id != page)
|
29
|
+
end
|
30
|
+
Hash === result ? result[:value] : nil
|
25
31
|
end
|
26
32
|
|
27
|
-
if
|
28
|
-
raise Capybara::TimeoutError,
|
33
|
+
if String === value
|
34
|
+
raise Capybara::TimeoutError, value
|
29
35
|
end
|
30
36
|
end
|
31
37
|
|
data/lib/terminus/proxy.rb
CHANGED
@@ -4,6 +4,7 @@ module Terminus
|
|
4
4
|
CONTENT_TYPES = %w[text/plain text/html]
|
5
5
|
BASIC_RESOURCES = %w[/favicon.ico /robots.txt]
|
6
6
|
MAX_REDIRECTS = 5
|
7
|
+
REDIRECT_CODES = [301, 302, 303, 305, 307]
|
7
8
|
|
8
9
|
INFINITE_REDIRECT_RESPONSE = [
|
9
10
|
200,
|
@@ -32,9 +33,11 @@ module Terminus
|
|
32
33
|
end
|
33
34
|
|
34
35
|
def call(env)
|
36
|
+
add_cookies(env)
|
35
37
|
response = @app.call(env)
|
38
|
+
store_cookies(env, response)
|
36
39
|
|
37
|
-
if response.first
|
40
|
+
if REDIRECT_CODES.include?(response.first)
|
38
41
|
@redirects += 1
|
39
42
|
if @redirects > MAX_REDIRECTS
|
40
43
|
@redirects = 0
|
@@ -44,7 +47,13 @@ module Terminus
|
|
44
47
|
@redirects = 0
|
45
48
|
end
|
46
49
|
|
50
|
+
if location = response[1].keys.grep(/^location$/i).first
|
51
|
+
app_host = URI.parse('http://' + env['HTTP_HOST']).host
|
52
|
+
response[1][location] = Terminus.rewrite_remote(response[1][location], app_host).to_s
|
53
|
+
end
|
54
|
+
|
47
55
|
return response if response.first == -1 or # async response
|
56
|
+
REDIRECT_CODES.include?(response.first) or # redirects
|
48
57
|
BASIC_RESOURCES.include?(env['PATH_INFO']) or # not pages - favicon etc
|
49
58
|
env.has_key?('HTTP_X_REQUESTED_WITH') # Ajax calls
|
50
59
|
|
@@ -58,5 +67,30 @@ module Terminus
|
|
58
67
|
response
|
59
68
|
end
|
60
69
|
|
70
|
+
private
|
71
|
+
|
72
|
+
def add_cookies(env)
|
73
|
+
return unless External === @app
|
74
|
+
cookies = Terminus.cookies.get_cookies(env['REQUEST_URI'])
|
75
|
+
env['HTTP_COOKIE'] = (cookies + [env['HTTP_COOKIE']]).compact.join('; ')
|
76
|
+
rescue => e
|
77
|
+
puts e.message
|
78
|
+
end
|
79
|
+
|
80
|
+
def store_cookies(env, response)
|
81
|
+
set_cookie = response[1].keys.grep(/^set-cookie$/i).first
|
82
|
+
return unless set_cookie
|
83
|
+
|
84
|
+
host = External === @app ? @app.uri.host : env['HTTP_HOST']
|
85
|
+
endpoint = "http://#{host}#{env['PATH_INFO']}"
|
86
|
+
|
87
|
+
[*response[1][set_cookie]].compact.each do |cookie|
|
88
|
+
cookie.split(/\s*,\s*(?=\S+=)/).each do |value|
|
89
|
+
Terminus.cookies.set_cookie(endpoint, value)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
61
94
|
end
|
62
95
|
end
|
96
|
+
|