terminus 0.1.0 → 0.2.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.
@@ -1,15 +1,308 @@
1
1
  Terminus = {
2
+ isIE: /\bMSIE\b/.test(navigator.userAgent),
3
+
2
4
  connect: function(endpoint) {
3
5
  if (this._client) return;
4
6
 
5
- this._client = new Faye.Client(endpoint);
7
+ this._pageId = Faye.random();
8
+
9
+ this._id = window.name = window.name || Faye.random();
10
+
11
+ this.Registry.initialize();
12
+ this.Worker.initialize();
13
+
14
+ var client = this._client = new Faye.Client(endpoint);
15
+
6
16
  this._client.subscribe('/terminus/commands', function(message) {
7
17
  this.execute(message.command);
8
18
  }, this);
19
+
20
+ this._client.subscribe('/terminus/clients/' + this._id, function(message) {
21
+ var command = message.command,
22
+ method = command.shift(),
23
+ driver = this.Driver,
24
+ worker = this.Worker,
25
+ self = this;
26
+
27
+ command.push(function(result) {
28
+ self.postResult(message.commandId, result);
29
+ });
30
+
31
+ worker.monitor = true;
32
+ driver[method].apply(driver, command);
33
+ worker.monitor = false;
34
+ }, this);
35
+
36
+ var self = this;
37
+ setTimeout(function() { self.ping() }, 100);
38
+ },
39
+
40
+ browserDetails: function() {
41
+ return {
42
+ id: this._id,
43
+ page: this._pageId,
44
+ ua: navigator.userAgent,
45
+ url: window.location.href
46
+ };
47
+ },
48
+
49
+ ping: function() {
50
+ this._client.publish('/terminus/ping', this.browserDetails());
51
+ var self = this;
52
+ setTimeout(function() { self.ping() }, 3000);
53
+ },
54
+
55
+ postResult: function(commandId, result) {
56
+ if (!commandId) return;
57
+
58
+ this._client.publish('/terminus/results', {
59
+ id: this._id,
60
+ commandId: commandId,
61
+ result: result
62
+ });
9
63
  },
10
64
 
11
65
  execute: function(command) {
12
66
  eval(command);
67
+ },
68
+
69
+ Driver: {
70
+ _node: function(id) {
71
+ return Terminus.Registry.get(id);
72
+ },
73
+
74
+ attribute: function(nodeId, name, callback) {
75
+ var node = this._node(nodeId);
76
+
77
+ if (name === 'checked' || name === 'selected')
78
+ return callback(!!node[name]);
79
+
80
+ if (Terminus.isIE)
81
+ return callback(node.getAttributeNode(name).nodeValue);
82
+
83
+ callback(node.getAttribute(name));
84
+ },
85
+
86
+ body: function(callback) {
87
+ var html = document.getElementsByTagName('html')[0];
88
+ callback(html.outerHTML ||
89
+ '<html>\n' + html.innerHTML + '\n</html>\n');
90
+ },
91
+
92
+ source: function(callback) {
93
+ callback(Terminus.originalSource);
94
+ },
95
+
96
+ reset: function(callback) {
97
+ var cookies = document.cookie.split(';'), name;
98
+
99
+ var expiry = new Date();
100
+ expiry.setTime(expiry.getTime() - 24*60*60*1000);
101
+
102
+ for (var i = 0, n = cookies.length; i < n; i++) {
103
+ name = cookies[i].split('=')[0];
104
+ document.cookie = name + '=; expires=' + expiry.toGMTString() + '; path=/';
105
+ }
106
+ callback();
107
+ },
108
+
109
+ click: function(nodeId, callback) {
110
+ var element = this._node(nodeId);
111
+ Syn.trigger('click', {}, element);
112
+ Terminus.Worker.callback(callback);
113
+ },
114
+
115
+ drag: function(options, callback) {
116
+ var draggable = this._node(options.from),
117
+ droppable = this._node(options.to);
118
+ Syn.drag({to: droppable}, draggable, callback);
119
+ },
120
+
121
+ evaluate: function(expression, callback) {
122
+ callback(eval(expression));
123
+ },
124
+
125
+ execute: function(expression, callback) {
126
+ eval(expression);
127
+ callback();
128
+ },
129
+
130
+ find: function(xpath, nodeId, callback) {
131
+ var root = this._node(nodeId) || document;
132
+
133
+ var result = document.evaluate(xpath, root, null, XPathResult.ANY_TYPE, null),
134
+ list = [],
135
+ element;
136
+
137
+ while (element = result.iterateNext())
138
+ list.push(Terminus.Registry.put(element));
139
+
140
+ return callback(list);
141
+ },
142
+
143
+ is_visible: function(nodeId, callback) {
144
+ var node = this._node(nodeId);
145
+
146
+ while (node.tagName.toLowerCase() !== 'body') {
147
+ if (node.style.display === 'none' || node.type === 'hidden')
148
+ return callback(false);
149
+ node = node.parentNode;
150
+ }
151
+ callback(true);
152
+ },
153
+
154
+ select: function(nodeId, callback) {
155
+ var option = this._node(nodeId);
156
+ option.selected = true;
157
+ Syn.trigger('change', {}, option.parentNode);
158
+ callback(true);
159
+ },
160
+
161
+ set: function(nodeId, value, callback) {
162
+ var field = this._node(nodeId);
163
+ if (field.type === 'file') return callback('not_allowed');
164
+
165
+ Syn.trigger('focus', {}, field);
166
+ Syn.trigger('click', {}, field);
167
+
168
+ switch (typeof value) {
169
+ case 'string': field.value = value; break;
170
+ case 'boolean': field.checked = value; break;
171
+ }
172
+ callback();
173
+ },
174
+
175
+ tag_name: function(nodeId, callback) {
176
+ callback(this._node(nodeId).tagName.toLowerCase());
177
+ },
178
+
179
+ text: function(nodeId, callback) {
180
+ var node = this._node(nodeId),
181
+ text = node.textContent || node.innerText || '';
182
+
183
+ text = text.replace(/^\s*|\s*$/g, '');
184
+ callback(text);
185
+ },
186
+
187
+ trigger: function(nodeId, eventType, callback) {
188
+ var node = this._node(nodeId);
189
+ Syn.trigger(eventType, {}, node);
190
+ callback();
191
+ },
192
+
193
+ unselect: function(nodeId, callback) {
194
+ var option = this._node(nodeId);
195
+ if (!option.parentNode.multiple) return callback(false);
196
+ option.selected = false;
197
+ Syn.trigger('change', {}, option.parentNode);
198
+ callback(true);
199
+ },
200
+
201
+ value: function(nodeId, callback) {
202
+ var node = this._node(nodeId);
203
+ if (node.tagName.toLowerCase() !== 'select' || !node.multiple)
204
+ return callback(node.value);
205
+
206
+ var options = node.children,
207
+ values = [];
208
+
209
+ for (var i = 0, n = options.length; i < n; i++) {
210
+ if (options[i].selected) values.push(options[i].value);
211
+ }
212
+ callback(values);
213
+ },
214
+
215
+ visit: function(url, callback) {
216
+ window.location.href = url;
217
+ callback();
218
+ }
219
+ },
220
+
221
+ Registry: {
222
+ initialize: function() {
223
+ this._namespace = new Faye.Namespace();
224
+ this._elements = {};
225
+ },
226
+
227
+ get: function(id) {
228
+ return this._elements[id];
229
+ },
230
+
231
+ put: function(element) {
232
+ var id = this._namespace.generate();
233
+ this._elements[id] = element;
234
+ return id;
235
+ }
236
+ },
237
+
238
+ Worker: {
239
+ initialize: function() {
240
+ this._callbacks = [];
241
+ this._pending = 0;
242
+
243
+ this._wrapTimeouts();
244
+ },
245
+
246
+ callback: function(callback, scope) {
247
+ if (this._pending === 0) return callback.call(scope);
248
+ else this._callbacks.push([callback, scope]);
249
+ },
250
+
251
+ suspend: function() {
252
+ this._pending += 1;
253
+ },
254
+
255
+ resume: function() {
256
+ if (this._pending === 0) return;
257
+ this._pending -= 1;
258
+ if (this._pending !== 0) return;
259
+
260
+ var callback;
261
+ for (var i = 0, n = this._callbacks.length; i < n; i++) {
262
+ callback = this._callbacks[i];
263
+ callback[0].call(callback[1]);
264
+ }
265
+ this._callbacks = [];
266
+ },
267
+
268
+ _wrapTimeouts: function() {
269
+ var timeout = window.setTimeout,
270
+ clear = window.clearTimeout,
271
+ timeouts = {},
272
+ self = this;
273
+
274
+ var finish = function(id) {
275
+ if (!timeouts.hasOwnProperty(id)) return;
276
+ delete timeouts[id];
277
+ self.resume();
278
+ };
279
+
280
+ window.setTimeout = function(callback, delay) {
281
+ var id = timeout(function() {
282
+ try {
283
+ switch (typeof callback) {
284
+ case 'function': callback(); break;
285
+ case 'string': eval(callback); break;
286
+ }
287
+ } catch (e) {
288
+ throw e;
289
+ } finally {
290
+ finish(id);
291
+ }
292
+ }, delay);
293
+
294
+ if (self.monitor) {
295
+ timeouts[id] = true;
296
+ self.suspend();
297
+ }
298
+ return id;
299
+ };
300
+
301
+ window.clearTimeout = function(id) {
302
+ finish(id);
303
+ return clear(id);
304
+ };
305
+ }
13
306
  }
14
307
  };
15
308
 
@@ -1,23 +1,67 @@
1
+ require 'forwardable'
2
+ require 'uri'
1
3
  require 'rubygems'
2
4
  require 'rack'
3
5
  require 'thin'
4
6
  require 'eventmachine'
5
7
  require 'faye'
8
+ require 'capybara'
6
9
  require 'sinatra'
7
10
  require 'packr'
8
11
 
9
- %w[application server].each do |file|
10
- require File.join(File.dirname(__FILE__), 'terminus', file)
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)
11
23
  end
12
24
 
25
+ require root + '/capybara/driver/terminus'
26
+
13
27
  Thin::Logging.silent = true
14
28
 
15
29
  module Terminus
16
- VERSION = '0.1.0'
17
- FAYE_MOUNT = '/messaging'
30
+ VERSION = '0.2.0'
31
+ FAYE_MOUNT = '/messaging'
32
+ DEFAULT_HOST = 'localhost'
33
+ DEFAULT_PORT = 7004
18
34
 
19
- def self.create(options = {})
20
- Server.new(options)
35
+ class << self
36
+ def create(options = {})
37
+ Server.new(options)
38
+ end
39
+
40
+ def driver_script(host = DEFAULT_HOST)
41
+ Application.driver_script(host)
42
+ end
43
+
44
+ def endpoint(host = DEFAULT_HOST)
45
+ "http://#{host}:#{DEFAULT_PORT}#{FAYE_MOUNT}"
46
+ end
47
+
48
+ def ensure_reactor_running
49
+ Thread.new { EM.run unless EM.reactor_running? }
50
+ while not EM.reactor_running?; end
51
+ end
52
+
53
+ extend Forwardable
54
+ def_delegators :controller, :browser,
55
+ :browser=,
56
+ :ensure_docked_browser,
57
+ :ensure_browser,
58
+ :return_to_dock
59
+
60
+ private
61
+
62
+ def controller
63
+ @controller ||= Controller.new
64
+ end
21
65
  end
22
66
  end
23
67
 
@@ -7,17 +7,22 @@ module Terminus
7
7
  set :public, ROOT + '/public'
8
8
  set :views, ROOT + '/views'
9
9
 
10
+ def self.driver_script(host)
11
+ %Q{<script type="text/javascript" src="http://#{host}:#{DEFAULT_PORT}/controller.js"></script>}
12
+ end
13
+
10
14
  helpers do
11
- def host
12
- "http://#{ env['HTTP_HOST'] }"
13
- end
14
-
15
15
  def bookmarklet
16
16
  Packr.pack(erb(:bookmarklet), :shrink_vars => true)
17
17
  end
18
+
19
+ def host
20
+ "http://#{ env['HTTP_HOST'] }"
21
+ end
18
22
  end
19
23
 
20
24
  get('/') { erb :index }
25
+ get('/controller.js') { bookmarklet }
21
26
 
22
27
  end
23
28
  end
@@ -0,0 +1,140 @@
1
+ module Terminus
2
+ class Browser
3
+
4
+ include Timeouts
5
+
6
+ LOCALHOST = /localhost|0\.0\.0\.0|127\.0\.0\.1/
7
+ RETRY_LIMIT = 3
8
+
9
+ def initialize(controller)
10
+ @controller = controller
11
+ @attributes = {}
12
+ @docked = false
13
+ @namespace = Faye::Namespace.new
14
+ @ping_callbacks = []
15
+ @results = {}
16
+ add_timeout(:dead, Timeouts::TIMEOUT) { drop_dead! }
17
+ end
18
+
19
+ def ask(command, retries = RETRY_LIMIT)
20
+ id = tell(command)
21
+ result_hash = wait_with_timeout(:result) { result(id) }
22
+ result_hash[:value]
23
+ rescue Timeouts::TimeoutError => e
24
+ raise e if retries.zero?
25
+ ask(command, retries - 1)
26
+ end
27
+
28
+ def body
29
+ ask([:body])
30
+ end
31
+
32
+ def source
33
+ ask([:source])
34
+ end
35
+
36
+ def current_path
37
+ return nil unless url = @attributes['url']
38
+ URI.parse(url).path
39
+ end
40
+
41
+ def current_url
42
+ @attributes['url']
43
+ end
44
+
45
+ def docked?
46
+ @docked
47
+ end
48
+
49
+ def evaluate_script(expression)
50
+ ask([:evaluate, expression])
51
+ end
52
+
53
+ def execute_script(expression)
54
+ tell([:execute, expression])
55
+ nil
56
+ end
57
+
58
+ def find(xpath)
59
+ ask([:find, xpath, false]).map { |id| Node.new(self, id) }
60
+ end
61
+
62
+ def id
63
+ @attributes['id']
64
+ end
65
+ alias :name :id
66
+
67
+ def page_id
68
+ @attributes['page']
69
+ end
70
+
71
+ def ping!(message)
72
+ remove_timeout(:dead)
73
+ add_timeout(:dead, Timeouts::TIMEOUT) { drop_dead! }
74
+ @attributes = @attributes.merge(message)
75
+ detect_dock_host
76
+ @ping = true
77
+ end
78
+
79
+ def reset!
80
+ ask([:reset])
81
+ end
82
+
83
+ def result!(message)
84
+ @results[message['commandId']] = message['result']
85
+ end
86
+
87
+ def result(id)
88
+ return nil unless @results.has_key?(id)
89
+ {:value => @results.delete(id)}
90
+ end
91
+
92
+ def return_to_dock
93
+ visit "http://#{@controller.dock_host}:#{DEFAULT_PORT}/"
94
+ end
95
+
96
+ def tell(command)
97
+ id = @namespace.generate
98
+ messenger.publish(channel, 'command' => command, 'commandId' => id)
99
+ id
100
+ end
101
+
102
+ def visit(url, retries = RETRY_LIMIT)
103
+ url = url.gsub(LOCALHOST, @controller.dock_host)
104
+ tell([:visit, url])
105
+ wait_for_ping
106
+ rescue Timeouts::TimeoutError => e
107
+ raise e if retries.zero?
108
+ visit(url, retries - 1)
109
+ end
110
+
111
+ def wait_for_ping
112
+ @ping = false
113
+ wait_with_timeout(:ping) { @ping or @dead }
114
+ end
115
+
116
+ private
117
+
118
+ def channel
119
+ "/terminus/clients/#{id}"
120
+ end
121
+
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
127
+ end
128
+
129
+ def drop_dead!
130
+ @dead = true
131
+ @controller.drop_browser(self)
132
+ end
133
+
134
+ def messenger
135
+ @controller.messenger
136
+ end
137
+
138
+ end
139
+ end
140
+