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
@@ -1,35 +1,41 @@
1
1
  = Terminus
2
2
 
3
- * http://terminus.jcoglan.com
4
-
5
- Terminus is an experimental Capybara driver implemented in client-side
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
-
3
+ {Terminus}[http://terminus.jcoglan.com] is a {Capybara}[https://github.com/jnicklas/capybara]
4
+ driver for real browsers. It lets you control your application in any browser on
5
+ any device (including {PhantomJS}[http://phantomjs.org/]), without needing
6
+ browser plugins. This allows several types of testing to be automated:
7
+
10
8
  * Cross-browser testing
9
+ * Headless testing
11
10
  * Multi-browser interaction e.g. messaging apps
12
- * Testing on remote machines, phones, iPads etc.
11
+ * Testing on remote machines, phones, iPads etc
12
+
13
+
14
+ == Running the example
15
+
16
+ Install the dependencies and boot the Terminus server, then open
17
+ {http://localhost:70004/}[http://localhost:70004/] in your browser.
13
18
 
19
+ bundle install
20
+ bundle exec bin/terminus
14
21
 
15
- == Notes / to-do
22
+ With your browser open, start an IRB session and begin controlling the app:
16
23
 
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.
20
-
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.
23
-
24
- * It can be a little brittle, and occasionally there seem to be race conditions
25
- when running the Capybara specs.
24
+ $ irb -r ./example/app
25
+ >> extend Capybara::DSL
26
+ >> visit '/'
27
+ >> click_link 'Sign up!'
28
+ >> fill_in 'Username', :with => 'jcoglan'
29
+ >> fill_in 'Password', :with => 'hello'
30
+ >> choose 'Web scale'
31
+ >> click_button 'Go!'
26
32
 
27
33
 
28
34
  == License
29
35
 
30
36
  (The MIT License)
31
37
 
32
- Copyright (c) 2010-2011 James Coglan
38
+ Copyright (c) 2010-2012 James Coglan
33
39
 
34
40
  Permission is hereby granted, free of charge, to any person obtaining a copy of
35
41
  this software and associated documentation files (the 'Software'), to deal in
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'rubygems'
4
4
  require 'oyster'
5
- require File.expand_path(File.dirname(__FILE__) + '/../lib/terminus')
5
+ require File.expand_path('../../lib/terminus', __FILE__)
6
6
 
7
7
  spec = Oyster.spec do
8
8
  name "terminus -- Control web browsers with Ruby"
@@ -1,24 +1,35 @@
1
1
  class Capybara::Driver::Terminus < Capybara::Driver::Base
2
2
  attr_reader :options
3
3
 
4
+ NULL_APP = lambda do |env|
5
+ [200, {'Content-Type' => 'text/html'}, ['']]
6
+ end
7
+
4
8
  def initialize(app = nil, options = {})
5
- raise ArgumentError.new if app.nil?
6
-
7
- @app = Terminus::Proxy[app]
9
+ @app = Terminus::Proxy[app || NULL_APP]
8
10
  @options = options
9
11
  @rack_server = Capybara::Server.new(@app)
10
12
 
11
13
  @rack_server.boot
14
+ sleep(0.1) until Terminus.server_running?(@rack_server)
12
15
  end
13
16
 
14
17
  def find(xpath)
15
18
  browser.find(xpath, self)
16
19
  end
17
20
 
21
+ def invalid_element_errors
22
+ [::Terminus::ObsoleteElementError]
23
+ end
24
+
18
25
  def visit(path)
19
26
  browser.visit(@rack_server.url(path))
20
27
  end
21
28
 
29
+ def wait?
30
+ true
31
+ end
32
+
22
33
  extend Forwardable
23
34
  def_delegators :browser, :body,
24
35
  :current_url,
@@ -35,18 +46,7 @@ class Capybara::Driver::Terminus < Capybara::Driver::Base
35
46
  yield
36
47
  Terminus.browser = current_browser
37
48
  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
47
- yield
48
- Terminus.browser = current_browser
49
- end
49
+ alias :within_frame :within_window
50
50
 
51
51
  private
52
52
 
@@ -56,11 +56,22 @@ private
56
56
  end
57
57
  end
58
58
 
59
- # Capybara 0.3.9 looks in the Capybara::Driver namespace for
60
- # appropriate drivers. 0.4 uses this registration API instead.
59
+ # Capybara 0.3.9 looks in the Capybara::Driver namespace for appropriate
60
+ # drivers. 0.4 uses this registration API instead.
61
61
  if Capybara.respond_to?(:register_driver)
62
62
  Capybara.register_driver :terminus do |app|
63
63
  Capybara::Driver::Terminus.new(app)
64
64
  end
65
65
  end
66
66
 
67
+ # We use WEBrick to boot the test app, because if we use Thin (the default) the
68
+ # slow response used to test Ajax resynchronization blocks the event loop. This
69
+ # stops Terminus receiving messages and causes false positives: the client is
70
+ # not really waiting for Ajax to complete, it's just having its messages blocked
71
+ # because EventMachine is frozen.
72
+
73
+ Capybara.server do |app, port|
74
+ handler = Rack::Handler.get('webrick')
75
+ handler.run(app, :Port => port, :AccessLog => [], :Logger => WEBrick::Log::new(nil, 0))
76
+ end
77
+
@@ -1,17 +1,20 @@
1
+ require 'erb'
1
2
  require 'forwardable'
3
+ require 'http/parser'
4
+ require 'net/http'
5
+ require 'rbconfig'
6
+ require 'socket'
7
+ require 'stringio'
2
8
  require 'uri'
3
- require 'rack'
4
- require 'thin'
9
+
10
+ require 'capybara'
11
+ require 'childprocess'
12
+ require 'cookiejar'
5
13
  require 'eventmachine'
6
14
  require 'faye'
7
- require 'sinatra'
8
- require 'packr'
9
- require 'capybara'
10
15
  require 'rack-proxy'
11
16
  require 'useragent'
12
17
 
13
- Thin::Logging.silent = true
14
-
15
18
  module Terminus
16
19
  FAYE_MOUNT = '/messaging'
17
20
  DEFAULT_HOST = 'localhost'
@@ -19,9 +22,11 @@ module Terminus
19
22
  LOCALHOST = /^(localhost|0\.0\.0\.0|127\.0\.0\.1)$/
20
23
  RETRY_LIMIT = 3
21
24
 
22
- ROOT = File.expand_path(File.dirname(__FILE__))
25
+ ROOT = File.expand_path('..', __FILE__)
23
26
  autoload :Application, ROOT + '/terminus/application'
24
27
  autoload :Browser, ROOT + '/terminus/browser'
28
+ autoload :Client, ROOT + '/terminus/client'
29
+ autoload :Connector, ROOT + '/terminus/connector'
25
30
  autoload :Controller, ROOT + '/terminus/controller'
26
31
  autoload :Host, ROOT + '/terminus/host'
27
32
  autoload :Node, ROOT + '/terminus/node'
@@ -31,24 +36,23 @@ module Terminus
31
36
 
32
37
  require ROOT + '/capybara/driver/terminus'
33
38
 
39
+ class ObsoleteElementError < StandardError
40
+ end
41
+
34
42
  class << self
35
- attr_accessor :debug
43
+ attr_accessor :debug, :sockets
36
44
 
37
45
  def create(options = {})
38
46
  Server.new(options)
39
47
  end
40
48
 
41
- def driver_script(host = DEFAULT_HOST)
42
- Application.driver_script(host)
43
- end
44
-
45
49
  def endpoint(host = DEFAULT_HOST)
46
50
  "http://#{host}:#{port}#{FAYE_MOUNT}"
47
51
  end
48
52
 
49
53
  def ensure_reactor_running
50
54
  Thread.new { EM.run unless EM.reactor_running? }
51
- while not EM.reactor_running?; end
55
+ Thread.pass until EM.reactor_running?
52
56
  end
53
57
 
54
58
  def port
@@ -59,14 +63,24 @@ module Terminus
59
63
  @port = port.to_i
60
64
  end
61
65
 
66
+ def start_browser(options = {})
67
+ Client::Browser.start(options)
68
+ end
69
+
70
+ def start_phantomjs(options = {})
71
+ Client::PhantomJS.start(options)
72
+ end
73
+
62
74
  extend Forwardable
63
75
  def_delegators :controller, :browser,
64
76
  :browsers,
65
77
  :browser=,
78
+ :cookies,
66
79
  :ensure_browsers,
67
80
  :return_to_dock,
68
81
  :rewrite_local,
69
- :rewrite_remote
82
+ :rewrite_remote,
83
+ :server_running?
70
84
 
71
85
  private
72
86
 
@@ -1,24 +1,18 @@
1
+ require 'packr'
2
+ require 'sinatra'
3
+
1
4
  module Terminus
2
5
  class Application < Sinatra::Base
3
6
 
4
- ROOT = File.expand_path(File.dirname(__FILE__) + '/../')
7
+ ROOT = File.expand_path('../..', __FILE__)
5
8
 
6
9
  set :static, true
7
- set :public, ROOT + '/terminus/public'
8
- set :views, ROOT + '/terminus/views'
9
-
10
- def self.driver_script(host)
11
- %Q{<script type="text/javascript" src="http://#{host}:#{Terminus.port}/bootstrap.js"></script>}
12
- end
10
+ set :root, ROOT + '/terminus'
13
11
 
14
12
  helpers do
15
13
  def bootstrap
16
14
  Packr.pack(erb(:bootstrap), :shrink_vars => true)
17
15
  end
18
-
19
- def host
20
- "http://#{ env['HTTP_HOST'] }"
21
- end
22
16
  end
23
17
 
24
18
  get '/' do
@@ -2,14 +2,15 @@ module Terminus
2
2
  class Browser
3
3
 
4
4
  include Timeouts
5
+ attr_reader :connector
6
+ attr_writer :sockets
5
7
 
6
8
  extend Forwardable
7
- def_delegator :@user_agent, :browser, :name
8
9
  def_delegators :@user_agent, :os, :version
9
10
 
10
- def initialize(controller)
11
+ def initialize(controller, id)
11
12
  @controller = controller
12
- @attributes = {}
13
+ @attributes = {'id' => id}
13
14
  @docked = false
14
15
  @frames = Set.new
15
16
  @namespace = Faye::Namespace.new
@@ -25,20 +26,32 @@ module Terminus
25
26
  return false unless @user_agent
26
27
 
27
28
  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
29
+ property = __send__(name)
30
+ value === property
33
31
  end
34
32
  end
35
33
 
36
34
  def ask(command, retries = RETRY_LIMIT)
37
- id = tell(command)
38
- result_hash = wait_with_timeout(:result) { result(id) }
39
- result_hash[:value]
35
+ debug(:ask, id, command)
36
+ value = if @connector
37
+ message = Yajl::Encoder.encode('commandId' => '_', 'command' => command)
38
+ response = @connector.request(message)
39
+ if response.nil?
40
+ retries == false ? false : ask(command)
41
+ else
42
+ result_hash = Yajl::Parser.parse(response)
43
+ result_hash['value']
44
+ end
45
+ else
46
+ command_id = tell(command)
47
+ result_hash = wait_with_timeout(:result) { result(command_id) }
48
+ result_hash[:value]
49
+ end
50
+ debug(:val, id, command, value)
51
+ raise ObsoleteElementError if value.nil?
52
+ value
40
53
  rescue Timeouts::TimeoutError => e
41
- raise e if retries.zero?
54
+ raise e if retries == 1
42
55
  ask(command, retries - 1)
43
56
  end
44
57
 
@@ -47,12 +60,18 @@ module Terminus
47
60
  end
48
61
 
49
62
  def current_path
50
- return nil unless url = @attributes['url']
51
- URI.parse(url).path
63
+ URI.parse(current_url).path
52
64
  end
53
65
 
54
66
  def current_url
55
- @attributes['url'] || ''
67
+ url = @attributes['url']
68
+ return '' unless url
69
+ return url unless @connector
70
+ rewrite_local(ask([:current_url]))
71
+ end
72
+
73
+ def debug(*args)
74
+ p args if Terminus.debug
56
75
  end
57
76
 
58
77
  def docked?
@@ -64,7 +83,7 @@ module Terminus
64
83
  end
65
84
 
66
85
  def execute_script(expression)
67
- tell([:execute, expression])
86
+ @connector ? ask([:execute, expression]) : tell([:execute, expression])
68
87
  nil
69
88
  end
70
89
 
@@ -80,25 +99,32 @@ module Terminus
80
99
  @frames.to_a
81
100
  end
82
101
 
83
- def frame_src(name)
84
- ask([:frame_src, name])
85
- end
86
-
87
102
  def id
88
103
  @attributes['id']
89
104
  end
90
105
 
106
+ def infinite_redirect?
107
+ return @infinite_redirect unless @connector
108
+ evaluate_script('!!window.TERMINUS_INFINITE_REDIRECT')
109
+ end
110
+
111
+ def name
112
+ return 'PhantomJS' if @user_agent.to_str =~ /\bPhantomJS\b/
113
+ @user_agent.browser
114
+ end
115
+
91
116
  def page_id
92
117
  @attributes['page']
93
118
  end
94
119
 
95
120
  def ping!(message)
96
- p message if Terminus.debug
121
+ debug(:ping, id)
122
+ debug(:recv, message)
123
+
97
124
  remove_timeout(:dead)
98
125
  add_timeout(:dead, Timeouts::TIMEOUT) { drop_dead! }
99
126
 
100
- uri = @controller.rewrite_local(message['url'], @dock_host)
101
- message['url'] = uri.to_s
127
+ message['url'] = rewrite_local(message['url'])
102
128
 
103
129
  @attributes = @attributes.merge(message)
104
130
  @user_agent = UserAgent.parse(message['ua'])
@@ -106,18 +132,20 @@ module Terminus
106
132
 
107
133
  @infinite_redirect = message['infinite']
108
134
 
109
- if parent = message['parent']
110
- @parent = Terminus.browser(parent)
135
+ if id =~ /\//
136
+ @parent = Terminus.browser(id.gsub(/\/[^\/]+$/, ''))
111
137
  @parent.frame!(self) unless @parent == self
112
138
  end
113
139
 
140
+ start_connector if message['sockets'] and sockets?
141
+
114
142
  @ping = true
115
143
  end
116
144
 
117
145
  def reset!
118
146
  if url = @attributes['url']
119
147
  uri = URI.parse(url)
120
- visit("http://#{uri.host}:#{uri.port}")
148
+ visit("http://#{uri.host}:#{uri.port}/")
121
149
  end
122
150
  ask([:clear_cookies])
123
151
  @attributes.delete('url')
@@ -128,17 +156,22 @@ module Terminus
128
156
  end
129
157
 
130
158
  def result!(message)
131
- p message if Terminus.debug
132
- @results[message['commandId']] = message['result']
159
+ debug(:result, id, message['commandId'], message['result'])
160
+ @results[message['commandId']] = {:value => message['result']}
133
161
  end
134
162
 
135
163
  def result(id)
136
164
  return nil unless @results.has_key?(id)
137
- {:value => @results.delete(id)}
165
+ @results.delete(id)
138
166
  end
139
167
 
140
168
  def return_to_dock
141
- visit "http://#{@dock_host}:#{Terminus.port}/"
169
+ return unless @dock_host
170
+ visit("http://#{@dock_host}:#{Terminus.port}/")
171
+ end
172
+
173
+ def sockets?
174
+ @sockets.nil? ? Terminus.sockets != false : @sockets
142
175
  end
143
176
 
144
177
  def source
@@ -150,20 +183,27 @@ module Terminus
150
183
  end
151
184
 
152
185
  def tell(command)
153
- id = @namespace.generate
154
- p [id, command] if Terminus.debug
155
- messenger.publish(channel, 'command' => command, 'commandId' => id)
156
- id
186
+ command_id = @namespace.generate
187
+ debug(:tell, id, command, command_id)
188
+ messenger.publish(command_channel, 'command' => command, 'commandId' => command_id)
189
+ command_id
157
190
  end
158
191
 
159
192
  def visit(url, retries = RETRY_LIMIT)
160
193
  close_frames!
161
194
  uri = @controller.rewrite_remote(url, @dock_host)
162
195
  uri.host = @dock_host if uri.host =~ LOCALHOST
163
- tell([:visit, uri.to_s])
164
- wait_for_ping
165
196
 
166
- if @infinite_redirect
197
+ if @connector
198
+ ask([:visit, uri.to_s], false)
199
+ @connector.drain_socket
200
+ @attributes['url'] = rewrite_local(uri)
201
+ else
202
+ tell([:visit, uri.to_s])
203
+ wait_for_ping
204
+ end
205
+
206
+ if infinite_redirect?
167
207
  @infinite_redirect = nil
168
208
  raise Capybara::InfiniteRedirectError
169
209
  end
@@ -188,16 +228,21 @@ module Terminus
188
228
  def drop_dead!
189
229
  remove_timeout(:dead)
190
230
  close_frames!
231
+ @connector.close if @connector
191
232
  @dead = true
192
233
  @controller.drop_browser(self)
193
234
  end
194
235
 
195
236
  private
196
237
 
197
- def channel
238
+ def command_channel
198
239
  "/terminus/clients/#{id}"
199
240
  end
200
241
 
242
+ def socket_channel
243
+ "/terminus/sockets/#{id}"
244
+ end
245
+
201
246
  def close_frames!
202
247
  @frames.each { |frame| frame.drop_dead! }
203
248
  @frames = Set.new
@@ -205,12 +250,20 @@ module Terminus
205
250
 
206
251
  def detect_dock_host
207
252
  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
253
+ @docked = (uri.port == Terminus.port)
254
+ @dock_host = @attributes['host']
255
+ end
256
+
257
+ def rewrite_local(url)
258
+ @controller.rewrite_local(url.to_s, @dock_host).to_s
259
+ end
260
+
261
+ def start_connector
262
+ return if @connector or @dock_host.nil? or Terminus.browser != self
263
+ @connector = Connector::Server.new(self)
264
+ url = "ws://#{@dock_host}:#{@connector.port}/"
265
+ debug(:connect, id, url)
266
+ messenger.publish(socket_channel, 'url' => url)
214
267
  end
215
268
 
216
269
  def messenger