terminus 0.2.0 → 0.3.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. data/README.rdoc +27 -89
  2. data/bin/terminus +7 -23
  3. data/lib/capybara/driver/terminus.rb +30 -11
  4. data/lib/terminus.rb +33 -22
  5. data/lib/terminus/application.rb +13 -7
  6. data/lib/terminus/browser.rb +110 -28
  7. data/lib/terminus/controller.rb +51 -20
  8. data/lib/terminus/host.rb +22 -0
  9. data/lib/terminus/node.rb +23 -4
  10. data/lib/terminus/proxy.rb +62 -0
  11. data/lib/terminus/proxy/driver_body.rb +63 -0
  12. data/lib/terminus/proxy/external.rb +25 -0
  13. data/lib/terminus/proxy/rewrite.rb +20 -0
  14. data/lib/terminus/public/icon.png +0 -0
  15. data/lib/terminus/public/loader.js +1 -0
  16. data/lib/terminus/public/style.css +43 -0
  17. data/lib/terminus/public/syn/browsers.js +150 -0
  18. data/lib/terminus/public/syn/drag/drag.js +322 -0
  19. data/lib/terminus/public/syn/key.js +905 -0
  20. data/lib/terminus/public/syn/mouse.js +284 -0
  21. data/lib/terminus/public/syn/synthetic.js +830 -0
  22. data/lib/{public → terminus/public}/terminus.js +109 -40
  23. data/lib/terminus/server.rb +5 -12
  24. data/lib/terminus/timeouts.rb +2 -2
  25. data/lib/{views/bookmarklet.erb → terminus/views/bootstrap.erb} +17 -12
  26. data/lib/terminus/views/index.erb +21 -0
  27. data/lib/terminus/views/infinite.html +16 -0
  28. data/spec/reports/chrome.txt +748 -0
  29. data/spec/reports/firefox.txt +748 -0
  30. data/spec/reports/opera.txt +748 -0
  31. data/spec/reports/safari.txt +748 -0
  32. data/spec/spec_helper.rb +18 -14
  33. data/spec/terminus_driver_spec.rb +7 -5
  34. data/spec/terminus_session_spec.rb +5 -18
  35. metadata +71 -57
  36. data/lib/public/loader.js +0 -1
  37. data/lib/public/style.css +0 -49
  38. data/lib/public/syn.js +0 -2355
  39. data/lib/views/index.erb +0 -32
@@ -1,112 +1,50 @@
1
1
  = Terminus
2
2
 
3
- * http://github.com/jcoglan/terminus
3
+ * http://terminus.jcoglan.com
4
4
 
5
5
  Terminus is an experimental Capybara driver implemented in client-side
6
- JavaScript. It lets you script your application in any browser on any
7
- device, without needing browser plugins. This allows several types of
8
- testing to be automated:
6
+ JavaScript. It lets you script your application in any browser on any device,
7
+ without needing browser plugins. This allows several types of testing to be
8
+ automated:
9
9
 
10
10
  * Cross-browser testing
11
11
  * Multi-browser interaction e.g. messaging apps
12
- * Run tests on remote machines, phones, iPads etc.
13
-
14
- It is also a remote scripting tool, giving you a REPL that lets you
15
- run JavaScript in any number of browsers at once.
16
-
17
-
18
- == Usage
19
-
20
- Terminus is a Capybara driver. For the most part, you will not use
21
- it directly: you will use the Capybara API and it will send instructions
22
- to Terminus for execution. To set Terminus as your driver:
23
-
24
- require 'capybara'
25
- require 'terminus'
26
-
27
- Capybara.current_driver = :terminus
28
-
29
- Terminus does require some extra setup before you can use it to control
30
- your app. First up, you need to start the Terminus server on the machine
31
- where your application will be running:
32
-
33
- $ terminus
34
-
35
- This starts the server on port 7004. Now open a browser at
36
- http://127.0.0.1:7004/. (I recommend using the IP address of the Terminus
37
- host; Chrome has bugs that can stop WebSockets working if you use the
38
- hostname.) This is the 'holding page'. A browser is said to be 'docked'
39
- while it is visiting this page, meaning it is ready and waiting to run
40
- some tests for you.
41
-
42
- To let Terminus control your app's pages, you need to include this
43
- just before the closing +body+ tag when in testing mode:
44
-
45
- <!-- For example if you're using Rails -->
46
-
47
- <% if Rails.env.test? %>
48
- <%= Terminus.driver_script '127.0.0.1' %>
49
- <% end %>
50
-
51
- If the browser you're using is on a different machine to the Terminus
52
- server, replace <tt>127.0.0.1</tt> with the Terminus machine's IP as
53
- seen by the browser. For example if I'm running the browser in VirtualBox
54
- and the Terminus server on the host OS, then I set the IP to <tt>10.0.2.2</tt>.
55
- You could also use <tt>request.host</tt> to get the right IP automatically
56
- if you're running your app and your Terminus server on the same machine.
57
-
58
- Finally, in your tests you need to make sure there's a docked browser
59
- and select it. In a 'before' block, run the following:
60
-
61
- Terminus.ensure_docked_browser
62
- Terminus.browser = :docked
63
-
64
- After each test is finished, you need to return the browser to the
65
- holding page to make it ready to accept new work. In an 'after' block:
66
-
67
- Terminus.return_to_dock
68
-
69
- This returns all currently connected browsers to the holding page.
12
+ * Testing on remote machines, phones, iPads etc.
70
13
 
71
14
 
72
15
  == Notes / to-do
73
16
 
74
- * Support IE, which has no built-in XPath engine for querying the DOM.
75
- I'm working on Pathology (see http://github.com/jcoglan/pathology) to
76
- try and fix this but it's currently not fast enough.
77
-
78
- * Allow <tt>Terminus.browser=</tt> to select browsers by name and
79
- version so we can control multiple browsers at once.
17
+ * Support IE, which has no built-in XPath engine for querying the DOM. I'm
18
+ working on Pathology (see http://github.com/jcoglan/pathology) to try and fix
19
+ this but it's currently not fast enough.
80
20
 
81
- * It's slow, especially at filling out forms. Use it to sanity-check
82
- your cross-browser code and JavaScript, not to test your whole app.
21
+ * It's slow, especially at filling out forms. Use it to sanity-check your
22
+ cross-browser code and JavaScript, not to test your whole app.
83
23
 
84
- * It can be a little brittle, and occasionally there seem to be race
85
- conditions when running the Capybara specs.
24
+ * It can be a little brittle, and occasionally there seem to be race conditions
25
+ when running the Capybara specs.
86
26
 
87
27
 
88
28
  == License
89
29
 
90
30
  (The MIT License)
91
31
 
92
- Copyright (c) 2010 James Coglan
32
+ Copyright (c) 2010-2011 James Coglan
93
33
 
94
- Permission is hereby granted, free of charge, to any person obtaining
95
- a copy of this software and associated documentation files (the
96
- 'Software'), to deal in the Software without restriction, including
97
- without limitation the rights to use, copy, modify, merge, publish,
98
- distribute, sublicense, and/or sell copies of the Software, and to
99
- permit persons to whom the Software is furnished to do so, subject to
100
- the following conditions:
34
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
35
+ this software and associated documentation files (the 'Software'), to deal in
36
+ the Software without restriction, including without limitation the rights to use,
37
+ copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
38
+ Software, and to permit persons to whom the Software is furnished to do so,
39
+ subject to the following conditions:
101
40
 
102
- The above copyright notice and this permission notice shall be
103
- included in all copies or substantial portions of the Software.
41
+ The above copyright notice and this permission notice shall be included in all
42
+ copies or substantial portions of the Software.
104
43
 
105
- THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
106
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
107
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
108
- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
109
- CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
110
- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
111
- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
44
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
45
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
46
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
47
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
48
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
49
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
112
50
 
@@ -5,7 +5,7 @@ require 'oyster'
5
5
  require File.expand_path(File.dirname(__FILE__) + '/../lib/terminus')
6
6
 
7
7
  spec = Oyster.spec do
8
- name "terminus -- controll web browsers from the command line"
8
+ name "terminus -- Control web browsers with Ruby"
9
9
  synopsis "terminus [--port PORT]"
10
10
 
11
11
  integer :port, :default => Terminus::DEFAULT_PORT
@@ -15,30 +15,14 @@ begin
15
15
  options = spec.parse
16
16
  app = Terminus.create(options)
17
17
 
18
- app.run!
19
- puts "Terminus server running on port #{options[:port]}"
20
- puts "Press CTRL-C to exit"
21
- puts ""
22
-
23
18
  trap("INT") { app.stop! ; exit }
24
19
 
25
- begin
26
- require 'readline'
27
-
28
- puts "This is the Terminus console. You can type JavaScript"
29
- puts "in here and it will be executed on all connected pages."
30
- puts ""
31
-
32
- loop {
33
- script, result = Readline.readline('>> '), nil
34
- Readline::HISTORY.push(script)
35
- app.execute(script) { |r| result = r }
36
- }
37
- rescue LoadError
38
- puts "If you install the readline library, you'll get a console"
39
- puts "that lets you execute JavaScript in connected browsers."
40
- puts ""
41
- end
20
+ Terminus.port = options[:port]
21
+
22
+ puts "Terminus server running on port #{Terminus.port}"
23
+ puts "Press CTRL-C to exit"
24
+ puts ""
25
+ app.run!
42
26
 
43
27
  rescue Oyster::HelpRendered
44
28
  end
@@ -1,30 +1,49 @@
1
- require 'forwardable'
2
-
3
1
  class Capybara::Driver::Terminus < Capybara::Driver::Base
4
- def initialize(app = nil)
2
+ attr_reader :options
3
+
4
+ def initialize(app = nil, options = {})
5
5
  raise ArgumentError.new if app.nil?
6
- @app = app
6
+
7
+ @app = Terminus::Proxy[app]
8
+ @options = options
7
9
  @rack_server = Capybara::Server.new(@app)
8
- @rack_server.boot if Capybara.run_server
10
+
11
+ @rack_server.boot
12
+ end
13
+
14
+ def find(xpath)
15
+ browser.find(xpath, self)
9
16
  end
10
17
 
11
18
  def visit(path)
12
- browser.visit @rack_server.url(path)
19
+ browser.visit(@rack_server.url(path))
13
20
  end
14
21
 
15
22
  extend Forwardable
16
23
  def_delegators :browser, :body,
17
- :current_path,
18
24
  :current_url,
19
25
  :evaluate_script,
20
26
  :execute_script,
21
- :find,
22
27
  :reset!,
23
- :source
28
+ :response_headers,
29
+ :source,
30
+ :status_code
24
31
 
25
32
  def within_window(name)
26
33
  current_browser = browser
27
- Terminus.browser = {:name => name}
34
+ Terminus.browser = browser.id + '/' + name
35
+ yield
36
+ Terminus.browser = current_browser
37
+ end
38
+
39
+ def within_frame(name)
40
+ frame_src = browser.frame_src(name)
41
+ frame = browser.frames.find do |frame|
42
+ frame.current_url == frame_src or
43
+ frame.current_path == frame_src
44
+ end
45
+ current_browser = browser
46
+ Terminus.browser = frame
28
47
  yield
29
48
  Terminus.browser = current_browser
30
49
  end
@@ -32,7 +51,7 @@ class Capybara::Driver::Terminus < Capybara::Driver::Base
32
51
  private
33
52
 
34
53
  def browser
35
- Terminus.ensure_browser
54
+ Terminus.ensure_browsers
36
55
  Terminus.browser
37
56
  end
38
57
  end
@@ -1,38 +1,39 @@
1
1
  require 'forwardable'
2
2
  require 'uri'
3
- require 'rubygems'
4
3
  require 'rack'
5
4
  require 'thin'
6
5
  require 'eventmachine'
7
6
  require 'faye'
8
- require 'capybara'
9
7
  require 'sinatra'
10
8
  require 'packr'
11
-
12
- root = File.expand_path(File.dirname(__FILE__))
13
-
14
- %w[ application
15
- server
16
- timeouts
17
- controller
18
- browser
19
- node
20
-
21
- ].each do |file|
22
- require File.join(root, 'terminus', file)
23
- end
24
-
25
- require root + '/capybara/driver/terminus'
9
+ require 'capybara'
10
+ require 'rack-proxy'
11
+ require 'useragent'
26
12
 
27
13
  Thin::Logging.silent = true
28
14
 
29
15
  module Terminus
30
- VERSION = '0.2.0'
31
16
  FAYE_MOUNT = '/messaging'
32
17
  DEFAULT_HOST = 'localhost'
33
18
  DEFAULT_PORT = 7004
19
+ LOCALHOST = /^(localhost|0\.0\.0\.0|127\.0\.0\.1)$/
20
+ RETRY_LIMIT = 3
21
+
22
+ ROOT = File.expand_path(File.dirname(__FILE__))
23
+ autoload :Application, ROOT + '/terminus/application'
24
+ autoload :Browser, ROOT + '/terminus/browser'
25
+ autoload :Controller, ROOT + '/terminus/controller'
26
+ autoload :Host, ROOT + '/terminus/host'
27
+ autoload :Node, ROOT + '/terminus/node'
28
+ autoload :Proxy, ROOT + '/terminus/proxy'
29
+ autoload :Server, ROOT + '/terminus/server'
30
+ autoload :Timeouts, ROOT + '/terminus/timeouts'
31
+
32
+ require ROOT + '/capybara/driver/terminus'
34
33
 
35
34
  class << self
35
+ attr_accessor :debug
36
+
36
37
  def create(options = {})
37
38
  Server.new(options)
38
39
  end
@@ -42,7 +43,7 @@ module Terminus
42
43
  end
43
44
 
44
45
  def endpoint(host = DEFAULT_HOST)
45
- "http://#{host}:#{DEFAULT_PORT}#{FAYE_MOUNT}"
46
+ "http://#{host}:#{port}#{FAYE_MOUNT}"
46
47
  end
47
48
 
48
49
  def ensure_reactor_running
@@ -50,12 +51,22 @@ module Terminus
50
51
  while not EM.reactor_running?; end
51
52
  end
52
53
 
54
+ def port
55
+ @port || DEFAULT_PORT
56
+ end
57
+
58
+ def port=(port)
59
+ @port = port.to_i
60
+ end
61
+
53
62
  extend Forwardable
54
63
  def_delegators :controller, :browser,
64
+ :browsers,
55
65
  :browser=,
56
- :ensure_docked_browser,
57
- :ensure_browser,
58
- :return_to_dock
66
+ :ensure_browsers,
67
+ :return_to_dock,
68
+ :rewrite_local,
69
+ :rewrite_remote
59
70
 
60
71
  private
61
72
 
@@ -4,16 +4,16 @@ module Terminus
4
4
  ROOT = File.expand_path(File.dirname(__FILE__) + '/../')
5
5
 
6
6
  set :static, true
7
- set :public, ROOT + '/public'
8
- set :views, ROOT + '/views'
7
+ set :public, ROOT + '/terminus/public'
8
+ set :views, ROOT + '/terminus/views'
9
9
 
10
10
  def self.driver_script(host)
11
- %Q{<script type="text/javascript" src="http://#{host}:#{DEFAULT_PORT}/controller.js"></script>}
11
+ %Q{<script type="text/javascript" src="http://#{host}:#{Terminus.port}/bootstrap.js"></script>}
12
12
  end
13
13
 
14
14
  helpers do
15
- def bookmarklet
16
- Packr.pack(erb(:bookmarklet), :shrink_vars => true)
15
+ def bootstrap
16
+ Packr.pack(erb(:bootstrap), :shrink_vars => true)
17
17
  end
18
18
 
19
19
  def host
@@ -21,8 +21,14 @@ module Terminus
21
21
  end
22
22
  end
23
23
 
24
- get('/') { erb :index }
25
- get('/controller.js') { bookmarklet }
24
+ get '/' do
25
+ erb :index
26
+ end
27
+
28
+ get '/bootstrap.js' do
29
+ headers 'Content-Type' => 'text/javascript'
30
+ bootstrap
31
+ end
26
32
 
27
33
  end
28
34
  end
@@ -3,19 +3,36 @@ module Terminus
3
3
 
4
4
  include Timeouts
5
5
 
6
- LOCALHOST = /localhost|0\.0\.0\.0|127\.0\.0\.1/
7
- RETRY_LIMIT = 3
6
+ extend Forwardable
7
+ def_delegator :@user_agent, :browser, :name
8
+ def_delegators :@user_agent, :os, :version
8
9
 
9
10
  def initialize(controller)
10
- @controller = controller
11
- @attributes = {}
12
- @docked = false
13
- @namespace = Faye::Namespace.new
14
- @ping_callbacks = []
15
- @results = {}
11
+ @controller = controller
12
+ @attributes = {}
13
+ @docked = false
14
+ @frames = Set.new
15
+ @namespace = Faye::Namespace.new
16
+ @results = {}
17
+
16
18
  add_timeout(:dead, Timeouts::TIMEOUT) { drop_dead! }
17
19
  end
18
20
 
21
+ def ===(params)
22
+ return docked? if params == :docked
23
+ return params == id if String === params
24
+ return false if @parent
25
+ return false unless @user_agent
26
+
27
+ params.all? do |name, value|
28
+ property = __send__(name).to_s
29
+ case value
30
+ when Regexp then property =~ value
31
+ when String then property == value
32
+ end
33
+ end
34
+ end
35
+
19
36
  def ask(command, retries = RETRY_LIMIT)
20
37
  id = tell(command)
21
38
  result_hash = wait_with_timeout(:result) { result(id) }
@@ -29,17 +46,13 @@ module Terminus
29
46
  ask([:body])
30
47
  end
31
48
 
32
- def source
33
- ask([:source])
34
- end
35
-
36
49
  def current_path
37
50
  return nil unless url = @attributes['url']
38
51
  URI.parse(url).path
39
52
  end
40
53
 
41
54
  def current_url
42
- @attributes['url']
55
+ @attributes['url'] || ''
43
56
  end
44
57
 
45
58
  def docked?
@@ -55,32 +68,67 @@ module Terminus
55
68
  nil
56
69
  end
57
70
 
58
- def find(xpath)
59
- ask([:find, xpath, false]).map { |id| Node.new(self, id) }
71
+ def find(xpath, driver = nil)
72
+ ask([:find, xpath, false]).map { |id| Node.new(self, id, driver) }
73
+ end
74
+
75
+ def frame!(frame_browser)
76
+ @frames.add(frame_browser)
77
+ end
78
+
79
+ def frames
80
+ @frames.to_a
81
+ end
82
+
83
+ def frame_src(name)
84
+ ask([:frame_src, name])
60
85
  end
61
86
 
62
87
  def id
63
88
  @attributes['id']
64
89
  end
65
- alias :name :id
66
90
 
67
91
  def page_id
68
92
  @attributes['page']
69
93
  end
70
94
 
71
95
  def ping!(message)
96
+ p message if Terminus.debug
72
97
  remove_timeout(:dead)
73
98
  add_timeout(:dead, Timeouts::TIMEOUT) { drop_dead! }
99
+
100
+ uri = @controller.rewrite_local(message['url'], @dock_host)
101
+ message['url'] = uri.to_s
102
+
74
103
  @attributes = @attributes.merge(message)
104
+ @user_agent = UserAgent.parse(message['ua'])
75
105
  detect_dock_host
106
+
107
+ @infinite_redirect = message['infinite']
108
+
109
+ if parent = message['parent']
110
+ @parent = Terminus.browser(parent)
111
+ @parent.frame!(self) unless @parent == self
112
+ end
113
+
76
114
  @ping = true
77
115
  end
78
116
 
79
117
  def reset!
80
- ask([:reset])
118
+ if url = @attributes['url']
119
+ uri = URI.parse(url)
120
+ visit("http://#{uri.host}:#{uri.port}")
121
+ end
122
+ ask([:clear_cookies])
123
+ @attributes.delete('url')
124
+ end
125
+
126
+ def response_headers
127
+ evaluate_script('TERMINUS_HEADERS')
81
128
  end
82
129
 
83
130
  def result!(message)
131
+ p message if Terminus.debug
84
132
  @results[message['commandId']] = message['result']
85
133
  end
86
134
 
@@ -90,19 +138,36 @@ module Terminus
90
138
  end
91
139
 
92
140
  def return_to_dock
93
- visit "http://#{@controller.dock_host}:#{DEFAULT_PORT}/"
141
+ visit "http://#{@dock_host}:#{Terminus.port}/"
142
+ end
143
+
144
+ def source
145
+ evaluate_script('TERMINUS_SOURCE')
146
+ end
147
+
148
+ def status_code
149
+ evaluate_script('TERMINUS_STATUS')
94
150
  end
95
151
 
96
152
  def tell(command)
97
153
  id = @namespace.generate
154
+ p [id, command] if Terminus.debug
98
155
  messenger.publish(channel, 'command' => command, 'commandId' => id)
99
156
  id
100
157
  end
101
158
 
102
159
  def visit(url, retries = RETRY_LIMIT)
103
- url = url.gsub(LOCALHOST, @controller.dock_host)
104
- tell([:visit, url])
160
+ close_frames!
161
+ uri = @controller.rewrite_remote(url, @dock_host)
162
+ uri.host = @dock_host if uri.host =~ LOCALHOST
163
+ tell([:visit, uri.to_s])
105
164
  wait_for_ping
165
+
166
+ if @infinite_redirect
167
+ @infinite_redirect = nil
168
+ raise Capybara::InfiniteRedirectError
169
+ end
170
+
106
171
  rescue Timeouts::TimeoutError => e
107
172
  raise e if retries.zero?
108
173
  visit(url, retries - 1)
@@ -113,22 +178,39 @@ module Terminus
113
178
  wait_with_timeout(:ping) { @ping or @dead }
114
179
  end
115
180
 
181
+ def to_s
182
+ "<#{self.class.name} #{name} #{version} (#{os})>"
183
+ end
184
+ alias :inspect :to_s
185
+
186
+ protected
187
+
188
+ def drop_dead!
189
+ remove_timeout(:dead)
190
+ close_frames!
191
+ @dead = true
192
+ @controller.drop_browser(self)
193
+ end
194
+
116
195
  private
117
196
 
118
197
  def channel
119
198
  "/terminus/clients/#{id}"
120
199
  end
121
200
 
122
- def detect_dock_host
123
- uri = URI.parse(@attributes['url'])
124
- return unless uri.port == DEFAULT_PORT
125
- @docked = true
126
- @controller.dock_host = uri.host
201
+ def close_frames!
202
+ @frames.each { |frame| frame.drop_dead! }
203
+ @frames = Set.new
127
204
  end
128
205
 
129
- def drop_dead!
130
- @dead = true
131
- @controller.drop_browser(self)
206
+ def detect_dock_host
207
+ uri = URI.parse(@attributes['url'])
208
+ if uri.port == Terminus.port
209
+ @docked = true
210
+ @dock_host = uri.host
211
+ else
212
+ @docked = false
213
+ end
132
214
  end
133
215
 
134
216
  def messenger