terminus 0.5.0 → 0.6.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 (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