terminus 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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
+