terminus 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +72 -0
  3. data/lib/capybara/driver/terminus.rb +14 -4
  4. data/lib/terminus.rb +2 -1
  5. data/lib/terminus/browser.rb +30 -7
  6. data/lib/terminus/client.rb +1 -1
  7. data/lib/terminus/client/phantomjs.rb +1 -1
  8. data/lib/terminus/connector.rb +120 -3
  9. data/lib/terminus/controller.rb +1 -1
  10. data/lib/terminus/node.rb +17 -3
  11. data/lib/terminus/proxy.rb +6 -4
  12. data/lib/terminus/proxy/external.rb +14 -9
  13. data/lib/terminus/public/compiled/terminus-min.js +3 -3
  14. data/lib/terminus/public/compiled/terminus.js +62 -17
  15. data/lib/terminus/public/loader.js +2 -1
  16. data/lib/terminus/public/terminus.js +62 -17
  17. data/lib/terminus/views/bootstrap.erb +3 -3
  18. data/lib/terminus/views/index.erb +1 -1
  19. metadata +40 -85
  20. data/README.rdoc +0 -56
  21. data/lib/terminus/connector/server.rb +0 -142
  22. data/lib/terminus/connector/socket_handler.rb +0 -72
  23. data/spec/1.1/reports/android.txt +0 -874
  24. data/spec/1.1/reports/chrome.txt +0 -880
  25. data/spec/1.1/reports/firefox.txt +0 -879
  26. data/spec/1.1/reports/opera.txt +0 -880
  27. data/spec/1.1/reports/phantomjs.txt +0 -874
  28. data/spec/1.1/reports/safari.txt +0 -880
  29. data/spec/1.1/spec_helper.rb +0 -31
  30. data/spec/1.1/terminus_driver_spec.rb +0 -24
  31. data/spec/1.1/terminus_session_spec.rb +0 -19
  32. data/spec/2.0/reports/android.txt +0 -815
  33. data/spec/2.0/reports/chrome.txt +0 -806
  34. data/spec/2.0/reports/firefox.txt +0 -812
  35. data/spec/2.0/reports/opera.txt +0 -806
  36. data/spec/2.0/reports/phantomjs.txt +0 -803
  37. data/spec/2.0/reports/safari.txt +0 -806
  38. data/spec/2.0/spec_helper.rb +0 -21
  39. data/spec/2.0/terminus_spec.rb +0 -25
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 30218e6e5c5e4b1e6511ce36a401a402889aaa21
4
+ data.tar.gz: 6dc3ecb9878543aaeb30294cefaee7cc87b16985
5
+ SHA512:
6
+ metadata.gz: b9a983a66f5310a7cec00f4979c0425908a4cc62e1e29a11e0476d18457efaf82de970f088516a9180c2a93585fdd8337edd1bc709a4f9654aca4025d32e6b05
7
+ data.tar.gz: 80e252b012aad3bcc0f3a01a50108e293c298a34aeddba1612c6233f7887e475f1c28f0d7d4a99aa0553169daa3573a5a302566d47c509877d90e8fc9299aa83
@@ -0,0 +1,72 @@
1
+ # Terminus
2
+
3
+ [Terminus[(http://terminus.jcoglan.com) is an experimental
4
+ [Capybara](https://github.com/jnicklas/capybara) driver for real browsers. It
5
+ lets you control your application in any browser on any device (including
6
+ [PhantomJS](http://phantomjs.org/)), without needing browser plugins. This
7
+ allows several types of testing to be automated:
8
+
9
+ * Cross-browser testing
10
+ * Headless testing
11
+ * Multi-browser interaction e.g. messaging apps
12
+ * Testing on remote machines, phones, iPads etc
13
+
14
+ *Since it is experimental, this project is sporadically maintained. Usage is
15
+ entirely at your own risk.*
16
+
17
+
18
+ ## Installation
19
+
20
+ ```
21
+ $ gem install terminus
22
+ ```
23
+
24
+
25
+ ## Running the example
26
+
27
+ Install the dependencies and boot the Terminus server, then open
28
+ http://localhost:70004/ in your browser.
29
+
30
+ ```
31
+ $ bundle install
32
+ $ bundle exec bin/terminus
33
+ ```
34
+
35
+ With your browser open, start an IRB session and begin controlling the app:
36
+
37
+ ```
38
+ $ irb -r ./example/app
39
+ >> extend Capybara::DSL
40
+ >> visit '/'
41
+ >> click_link 'Sign up!'
42
+ >> fill_in 'Username', :with => 'jcoglan'
43
+ >> fill_in 'Password', :with => 'hello'
44
+ >> choose 'Web scale'
45
+ >> click_button 'Go!'
46
+ ```
47
+
48
+
49
+ ## License
50
+
51
+ (The MIT License)
52
+
53
+ Copyright (c) 2010-2013 James Coglan
54
+
55
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
56
+ this software and associated documentation files (the 'Software'), to deal in
57
+ the Software without restriction, including without limitation the rights to
58
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
59
+ of the Software, and to permit persons to whom the Software is furnished to do
60
+ so, subject to the following conditions:
61
+
62
+ The above copyright notice and this permission notice shall be included in all
63
+ copies or substantial portions of the Software.
64
+
65
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
66
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
67
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
68
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
69
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
70
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
71
+ SOFTWARE.
72
+
@@ -15,10 +15,15 @@ class Capybara::Driver::Terminus < Capybara::Driver::Base
15
15
  Terminus.register_local_port(@rack_server.port)
16
16
  end
17
17
 
18
- def find(xpath)
19
- browser.find(xpath, self)
18
+ def find_css(css)
19
+ browser.find_css(css, self)
20
20
  end
21
21
 
22
+ def find_xpath(xpath)
23
+ browser.find_xpath(xpath, self)
24
+ end
25
+ alias :find :find_xpath
26
+
22
27
  def invalid_element_errors
23
28
  [::Terminus::ObsoleteElementError]
24
29
  end
@@ -43,14 +48,18 @@ class Capybara::Driver::Terminus < Capybara::Driver::Base
43
48
  :debugger,
44
49
  :evaluate_script,
45
50
  :execute_script,
51
+ :go_back,
52
+ :go_forward,
46
53
  :html,
47
54
  :reset!,
48
55
  :response_headers,
49
56
  :save_screenshot,
50
57
  :source,
51
- :status_code
58
+ :status_code,
59
+ :title
52
60
 
53
61
  def within_window(name)
62
+ name = name['id'] unless String === name
54
63
  current_browser = browser
55
64
  Terminus.browser = browser.id + '/' + name
56
65
  result = yield
@@ -83,6 +92,7 @@ end
83
92
 
84
93
  Capybara.server do |app, port|
85
94
  handler = Rack::Handler.get('webrick')
86
- handler.run(app, :Port => port, :AccessLog => [], :Logger => WEBrick::Log::new(nil, 0))
95
+ logger = Terminus.debug ? Logger.new(STDOUT) : WEBrick::Log.new(nil, 0)
96
+ handler.run(app, :Port => port, :AccessLog => [], :Logger => logger)
87
97
  end
88
98
 
@@ -1,6 +1,7 @@
1
1
  require 'erb'
2
2
  require 'forwardable'
3
3
  require 'http/parser'
4
+ require 'logger'
4
5
  require 'net/http'
5
6
  require 'rbconfig'
6
7
  require 'readline'
@@ -18,7 +19,7 @@ require 'useragent'
18
19
 
19
20
  module Terminus
20
21
  FAYE_MOUNT = '/messaging'
21
- DEFAULT_HOST = 'localhost'
22
+ DEFAULT_HOST = '127.0.0.1'
22
23
  DEFAULT_PORT = 7004
23
24
  LOCALHOST = /^(localhost|0\.0\.0\.0|127\.0\.0\.1)$/
24
25
  PING_PATH = '/favicon.ico'
@@ -34,12 +34,12 @@ module Terminus
34
34
  def ask(command, retries = RETRY_LIMIT)
35
35
  debug(:ask, id, command)
36
36
  value = if @connector
37
- message = Yajl::Encoder.encode('commandId' => '_', 'command' => command)
37
+ message = MultiJson.dump('commandId' => '_', 'command' => command)
38
38
  response = @connector.request(message)
39
39
  if response.nil?
40
40
  retries == false ? false : ask(command)
41
41
  else
42
- result_hash = Yajl::Parser.parse(response)
42
+ result_hash = MultiJson.load(response)
43
43
  result_hash['value']
44
44
  end
45
45
  else
@@ -87,8 +87,14 @@ module Terminus
87
87
  nil
88
88
  end
89
89
 
90
- def find(xpath, driver = nil)
91
- ask([:find, xpath, false]).map { |id| Node.new(self, id, driver) }
90
+ def find_css(css, driver = nil)
91
+ return [] unless @find_enabled
92
+ ask([:find_css, css, false]).map { |id| Node.new(self, id, driver) }
93
+ end
94
+
95
+ def find_xpath(xpath, driver = nil)
96
+ return [] unless @find_enabled
97
+ ask([:find_xpath, xpath, false]).map { |id| Node.new(self, id, driver) }
92
98
  end
93
99
 
94
100
  def frame!(frame_browser)
@@ -99,6 +105,14 @@ module Terminus
99
105
  @frames.to_a
100
106
  end
101
107
 
108
+ def go_back
109
+ execute_script('window.history.back()')
110
+ end
111
+
112
+ def go_forward
113
+ execute_script('window.history.forward()')
114
+ end
115
+
102
116
  def html
103
117
  ask([:body])
104
118
  end
@@ -132,8 +146,10 @@ module Terminus
132
146
  @attributes['raw_url'] = message['url']
133
147
  message['url'] = rewrite_local(message['url'])
134
148
 
135
- @attributes = @attributes.merge(message)
136
- @user_agent = UserAgent.parse(message['ua'])
149
+ @attributes = @attributes.merge(message)
150
+ @find_enabled = true
151
+ @user_agent = UserAgent.parse(message['ua'].gsub(/.*?\bOPR\b/, 'Opera'))
152
+
137
153
  detect_dock_host
138
154
 
139
155
  @infinite_redirect = message['infinite']
@@ -160,6 +176,7 @@ module Terminus
160
176
  ask([:clear_cookies])
161
177
 
162
178
  @attributes.delete('url')
179
+ @find_enabled = false
163
180
  end
164
181
 
165
182
  def response_headers
@@ -205,12 +222,18 @@ module Terminus
205
222
  command_id
206
223
  end
207
224
 
225
+ def title
226
+ ask([:title])
227
+ end
228
+
208
229
  def visit(url, retries = RETRY_LIMIT)
209
230
  close_frames!
210
231
  uri = @controller.rewrite_remote(url, @dock_host)
211
232
  uri.host = @dock_host if uri.host =~ LOCALHOST
212
233
  @controller.visit_url(uri.to_s)
213
234
 
235
+ @find_enabled = true
236
+
214
237
  if @connector
215
238
  ask([:visit, uri.to_s], false)
216
239
  @connector.drain_socket
@@ -277,7 +300,7 @@ module Terminus
277
300
 
278
301
  def start_connector
279
302
  return if @connector or @dock_host.nil? or Terminus.browser != self
280
- @connector = Connector::Server.new(self)
303
+ @connector = Connector.new(self)
281
304
  url = "ws://#{@dock_host}:#{@connector.port}/"
282
305
  debug(:connect, id, url)
283
306
  messenger.publish(socket_channel, 'url' => url)
@@ -25,7 +25,7 @@ module Terminus
25
25
  @id = Faye.random
26
26
  @options = options
27
27
  @address = TCPServer.new(0).addr
28
- @connector = Connector::Server.new(self)
28
+ @connector = Connector.new(self)
29
29
  @port = options[:port] || @address[1]
30
30
  @terminus = Terminus.create(:port => @port)
31
31
  @browser = ChildProcess.build(*browser_args)
@@ -47,7 +47,7 @@ module Terminus
47
47
  end
48
48
 
49
49
  def save_screenshot(path, options = {})
50
- message = Yajl::Encoder.encode(['save_screenshot', path, options])
50
+ message = MultiJson.dump(['save_screenshot', path, options])
51
51
  @connector.request(message)
52
52
  end
53
53
  end
@@ -1,8 +1,125 @@
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
+ require 'websocket/driver'
25
+
1
26
  module Terminus
2
- module Connector
27
+ class Connector
28
+
29
+ RECV_SIZE = 1024
30
+ BIND_TIMEOUT = 5
31
+
32
+ def initialize(browser, timeout = BIND_TIMEOUT)
33
+ @browser = browser
34
+ @skips = 0
35
+ @server = start_server
36
+ @timeout = timeout
37
+ reset
38
+ end
39
+
40
+ def reset
41
+ @closing = false
42
+ @driver = nil
43
+ @socket = nil
44
+ end
45
+
46
+ def connected?
47
+ not @socket.nil?
48
+ end
49
+
50
+ def port
51
+ @server.addr[1]
52
+ end
53
+
54
+ def request(message)
55
+ @browser.debug(:send, @browser.id, message)
56
+ accept unless connected?
57
+ @driver.text(message)
58
+ true while @closing && receive
59
+ result = receive
60
+ @browser.debug(:recv, @browser.id, result)
61
+ reset if result.nil?
62
+ result
63
+ rescue Errno::EBADF, Errno::ECONNRESET, Errno::EPIPE, Errno::EWOULDBLOCK
64
+ reset
65
+ nil
66
+ end
67
+
68
+ def drain_socket
69
+ @closing = true if @socket
70
+ end
71
+
72
+ def close
73
+ [@server, @socket].compact.each do |s|
74
+ s.close_read
75
+ s.close_write
76
+ end
77
+ end
78
+
79
+ def write(data)
80
+ @socket.write(data)
81
+ end
82
+
83
+ private
84
+
85
+ def start_server
86
+ time = Time.now
87
+ TCPServer.open(0)
88
+ rescue Errno::EADDRINUSE
89
+ if (Time.now - time) < BIND_TIMEOUT
90
+ sleep(0.01)
91
+ retry
92
+ else
93
+ raise
94
+ end
95
+ end
96
+
97
+ def accept
98
+ @skips.times { @server.accept.close }
99
+
100
+ @socket = @server.accept
101
+ @messages = []
102
+ @driver = WebSocket::Driver.server(self)
103
+
104
+ @driver.on(:connect) { |e| @driver.start }
105
+ @driver.on(:message) { |e| @messages << e.data }
106
+
107
+ # @browser.debug(:accept, @browser.id, @handler.url)
108
+ end
109
+
110
+ def receive
111
+ @browser.debug(:receive, @browser.id)
112
+ start = Time.now
3
113
 
4
- autoload :Server, ROOT + '/terminus/connector/server'
5
- autoload :SocketHandler, ROOT + '/terminus/connector/socket_handler'
114
+ until @messages.any?
115
+ raise Errno::EWOULDBLOCK if (Time.now - start) >= @timeout
116
+ IO.select([@socket], [], [], @timeout) or raise Errno::EWOULDBLOCK
117
+ data = @socket.recv(RECV_SIZE)
118
+ break if data.empty?
119
+ @driver.parse(data)
120
+ end
121
+ @messages.shift
122
+ end
6
123
 
7
124
  end
8
125
  end
@@ -55,7 +55,7 @@ module Terminus
55
55
 
56
56
  def register_local_port(port)
57
57
  @local_ports << port
58
- @host_aliases[Host.new(URI.parse("http://localhost:#{port}/"))] = Host.new(URI.parse("http://localhost:80/"))
58
+ @host_aliases[Host.new(URI.parse("http://127.0.0.1:#{port}/"))] = Host.new(URI.parse("http://127.0.0.1:80/"))
59
59
  end
60
60
 
61
61
  def return_to_dock
@@ -40,10 +40,15 @@ module Terminus
40
40
  end
41
41
  alias :eql? :==
42
42
 
43
- def find(xpath)
44
- @browser.ask([:find, xpath, @id]).map { |id| Node.new(@browser, id) }
43
+ def find_css(css)
44
+ @browser.ask([:find_css, css, @id]).map { |id| Node.new(@browser, id, @driver) }
45
45
  end
46
46
 
47
+ def find_xpath(xpath)
48
+ @browser.ask([:find_xpath, xpath, @id]).map { |id| Node.new(@browser, id, @driver) }
49
+ end
50
+ alias :find :find_xpath
51
+
47
52
  def hash
48
53
  @id.hash
49
54
  end
@@ -66,6 +71,15 @@ module Terminus
66
71
  raise Capybara::NotSupportedByDriverError.new if result == 'not_allowed'
67
72
  end
68
73
 
74
+ def all_text
75
+ @browser.ask([:text, @id, false])
76
+ end
77
+
78
+ def visible_text
79
+ @browser.ask([:text, @id, true])
80
+ end
81
+ alias :text :visible_text
82
+
69
83
  def trigger(event_type)
70
84
  @browser.ask([:trigger, @id, event_type])
71
85
  end
@@ -85,8 +99,8 @@ module Terminus
85
99
 
86
100
  SYNC_DSL_METHODS = [ [:[], :attribute],
87
101
  [:[]=, :set_attribute],
102
+ [:disabled?, :is_disabled],
88
103
  :tag_name,
89
- :text,
90
104
  :value,
91
105
  [:visible?, :is_visible]
92
106
  ]
@@ -40,7 +40,7 @@ module Terminus
40
40
 
41
41
  def self.content_type(response)
42
42
  type = response[1].find { |key, _| key =~ /^content-type$/i }
43
- type && type.last.split(';').first
43
+ type && type.flatten.last.split(';').first
44
44
  end
45
45
 
46
46
  def initialize(app)
@@ -69,7 +69,7 @@ module Terminus
69
69
  set_cookie = response[1].keys.grep(/^set-cookie$/i).first
70
70
  return unless set_cookie
71
71
 
72
- host = External === @app ? @app.uri.host : env['HTTP_HOST']
72
+ host = External === @app ? @app.host : env['HTTP_HOST']
73
73
  endpoint = "http://#{host}#{env['PATH_INFO']}"
74
74
 
75
75
  [*response[1][set_cookie]].compact.each do |cookie|
@@ -100,11 +100,13 @@ module Terminus
100
100
  end
101
101
 
102
102
  def rewrite_response(env, response)
103
+ headers = response[1]
104
+
105
+ headers.each_key { |k| headers[k] = [headers[k]].flatten.first }
103
106
  rewrite_location(env, response)
104
107
  return response if return_unmodified?(env, response)
105
108
 
106
- response[1] = response[1].dup
107
- response[1].delete_if { |key, _| key =~ /^content-length$/i }
109
+ headers.delete_if { |key, _| key =~ /^content-length$/i }
108
110
  response[2] = DriverBody.new(env, response)
109
111
  response
110
112
  end