terminus 0.2.0 → 0.3.0

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