terminus 0.3.0 → 0.4.0

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.
Files changed (44) hide show
  1. data/README.rdoc +25 -19
  2. data/bin/terminus +1 -1
  3. data/lib/capybara/driver/terminus.rb +28 -17
  4. data/lib/terminus.rb +29 -15
  5. data/lib/terminus/application.rb +5 -11
  6. data/lib/terminus/browser.rb +97 -44
  7. data/lib/terminus/client.rb +43 -0
  8. data/lib/terminus/client/browser.rb +30 -0
  9. data/lib/terminus/client/phantom.js +6 -0
  10. data/lib/terminus/client/phantomjs.rb +20 -0
  11. data/lib/terminus/connector.rb +9 -0
  12. data/lib/terminus/connector/server.rb +142 -0
  13. data/lib/terminus/connector/socket_handler.rb +72 -0
  14. data/lib/terminus/controller.rb +22 -5
  15. data/lib/terminus/host.rb +7 -2
  16. data/lib/terminus/node.rb +11 -5
  17. data/lib/terminus/proxy.rb +35 -1
  18. data/lib/terminus/proxy/driver_body.rb +26 -6
  19. data/lib/terminus/proxy/external.rb +9 -0
  20. data/lib/terminus/proxy/rewrite.rb +4 -2
  21. data/lib/terminus/public/compiled/terminus-min.js +3 -0
  22. data/lib/terminus/public/compiled/terminus.js +5270 -0
  23. data/lib/terminus/public/loader.js +1 -1
  24. data/lib/terminus/public/pathology.js +3174 -0
  25. data/lib/terminus/public/syn/browsers.js +2 -2
  26. data/lib/terminus/public/syn/drag/drag.js +3 -4
  27. data/lib/terminus/public/syn/key.js +130 -111
  28. data/lib/terminus/public/syn/mouse.js +2 -2
  29. data/lib/terminus/public/syn/synthetic.js +45 -34
  30. data/lib/terminus/public/terminus.js +183 -70
  31. data/lib/terminus/server.rb +6 -0
  32. data/lib/terminus/timeouts.rb +4 -2
  33. data/lib/terminus/views/bootstrap.erb +12 -27
  34. data/lib/terminus/views/index.erb +1 -1
  35. data/spec/reports/android.txt +875 -0
  36. data/spec/reports/chrome.txt +137 -8
  37. data/spec/reports/firefox.txt +137 -9
  38. data/spec/reports/opera.txt +142 -13
  39. data/spec/reports/phantomjs.txt +871 -0
  40. data/spec/reports/safari.txt +137 -8
  41. data/spec/spec_helper.rb +19 -17
  42. data/spec/terminus_driver_spec.rb +8 -6
  43. data/spec/terminus_session_spec.rb +4 -4
  44. 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,6 @@
1
+ var page = new WebPage(),
2
+ host = phantom.args[0],
3
+ port = phantom.args[1];
4
+
5
+ page.open('http://' + host + ':' + port + '/');
6
+
@@ -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,9 @@
1
+ module Terminus
2
+ module Connector
3
+
4
+ autoload :Server, ROOT + '/terminus/connector/server'
5
+ autoload :SocketHandler, ROOT + '/terminus/connector/socket_handler'
6
+
7
+ end
8
+ end
9
+
@@ -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
+
@@ -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.host = remote_host.host
66
- uri.port = remote_host.port
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.host, uri.port = server.host, server.port
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 0.1 until server.port
113
+ sleep(0.1) until server_running?(server)
97
114
  Host.new(server)
98
115
  end
99
116
  end
@@ -4,8 +4,13 @@ module Terminus
4
4
  attr_reader :host, :port
5
5
 
6
6
  def initialize(uri)
7
- @host = uri.host
8
- @port = uri.port
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)
@@ -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
- result = @browser.wait_with_timeout(:click_response) do
24
- @browser.result(command) || (@browser.page_id != page)
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 Hash === result and String === result[:value]
28
- raise Capybara::TimeoutError, result[:value]
33
+ if String === value
34
+ raise Capybara::TimeoutError, value
29
35
  end
30
36
  end
31
37
 
@@ -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 == 302
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
+