terminus 0.1.0 → 0.2.0

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