terminus 0.3.0 → 0.4.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 (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,11 +1,17 @@
1
1
  Terminus = {
2
2
  isIE: /\bMSIE\b/.test(navigator.userAgent),
3
3
 
4
- connect: function(endpoint) {
5
- if (this._client) return;
4
+ connect: function(host, port) {
5
+ if (this._bayeux) return;
6
6
 
7
+ this._host = host;
7
8
  this._pageId = Faye.random();
8
- this._id = window.name = window.name || Faye.random();
9
+ this._id = window.name = window.name || document.name || Faye.random();
10
+ this._id = this._id.split('|')[0];
11
+
12
+ var iframes = document.getElementsByTagName('iframe'), i = iframes.length;
13
+ while (i--)
14
+ iframes[i].contentDocument.name = iframes[i].id;
9
15
 
10
16
  this.Registry.initialize();
11
17
  this.Worker.initialize();
@@ -13,67 +19,141 @@ Terminus = {
13
19
 
14
20
  Faye.Event.on(window, 'beforeunload', function() { Terminus.disabled = true });
15
21
 
16
- var client = this._client = new Faye.Client(endpoint);
22
+ var endpoint = 'http://' + host + ':' + port + '/messaging',
23
+ bayeux = this._bayeux = new Faye.Client(endpoint),
24
+ self = this;
25
+
26
+ bayeux.addExtension({
27
+ outgoing: function(message, callback) {
28
+ message.href = window.location.href;
29
+ if (message.connectionType === 'websocket') self._socketCapable = true;
30
+ callback(message);
31
+ }
32
+ });
17
33
 
18
- var subscription = this._client.subscribe('/terminus/clients/' + this.getId(), function(message) {
19
- var command = message.command,
20
- method = command.shift(),
21
- driver = this.Driver,
22
- worker = this.Worker,
23
- posted = false,
24
- self = this;
34
+ this.getId(function(id) {
35
+ var url = window.name.split('|')[1];
25
36
 
26
- command.push(function(result) {
27
- if (posted) return;
28
- self.postResult(message.commandId, result);
29
- posted = true;
30
- });
37
+ if (!url)
38
+ bayeux.subscribe('/terminus/sockets/' + id, function(message) {
39
+ window.name += '|' + message.url;
40
+ this.openSocket(message.url);
41
+ }, this);
31
42
 
32
- worker.monitor = true;
33
- driver[method].apply(driver, command);
34
- worker.monitor = false;
43
+ var sub = bayeux.subscribe('/terminus/clients/' + id, this.handleMessage, this);
44
+ sub.callback(function() {
45
+ this.ping();
46
+ if (url) this.openSocket(url);
47
+ }, this);
35
48
  }, this);
36
-
37
- subscription.callback(this.ping, this);
38
49
  },
39
50
 
40
- getId: function() {
41
- try { return window.opener.Terminus.getId() + '/' + this._id }
42
- catch (e) { return this._id }
51
+ browserDetails: function(callback, context) {
52
+ this.getId(function(id) {
53
+ callback.call(context, {
54
+ host: this._host,
55
+ id: id,
56
+ infinite: !!window.TERMINUS_INFINITE_REDIRECT,
57
+ page: this._pageId,
58
+ sockets: this._socketCapable,
59
+ ua: navigator.userAgent,
60
+ url: window.location.href
61
+ });
62
+ }, this);
43
63
  },
44
64
 
45
- browserDetails: function() {
46
- return {
47
- id: this.getId(),
48
- parent: (parent && parent !== window) ? parent.name : null,
49
- page: this._pageId,
50
- ua: navigator.userAgent,
51
- url: window.location.href,
52
- infinite: !!window.TERMINUS_INFINITE_REDIRECT
53
- };
65
+ getId: function(callback, context) {
66
+ var id = this._id;
67
+ if (this.isIE) return callback.call(context, id);
68
+
69
+ if (opener && opener.Terminus) {
70
+ opener.Terminus.getId(function(prefix) {
71
+ callback.call(context, prefix + '/' + id);
72
+ });
73
+ } else if (parent && parent !== window) {
74
+ var getParentId = function() {
75
+ if (!parent.Terminus) return setTimeout(getParentId, 100);
76
+ parent.Terminus.getId(function(prefix) {
77
+ callback.call(context, prefix + '/' + id);
78
+ });
79
+ };
80
+ getParentId();
81
+ } else {
82
+ callback.call(context, id);
83
+ }
54
84
  },
55
85
 
56
- getAttribute: function(node, name) {
57
- return Terminus.isIE ? node.getAttributeNode(name).nodeValue
58
- : node.getAttribute(name);
86
+ openSocket: function(endpoint) {
87
+ if (this.disabled || this._socket) return;
88
+
89
+ var self = this,
90
+ WS = window.MozWebSocket || window.WebSocket,
91
+ ws = new WS(endpoint);
92
+
93
+ ws.onopen = function() {
94
+ self._socket = ws;
95
+ up = true;
96
+ };
97
+ ws.onclose = function() {
98
+ var up = !!self._socket;
99
+ self._socket = null;
100
+ if (up)
101
+ self.openSocket(endpoint);
102
+ else
103
+ window.name = window.name.split('|')[0];
104
+ };
105
+ ws.onmessage = function(event) {
106
+ self.handleMessage(JSON.parse(event.data));
107
+ };
59
108
  },
60
109
 
61
110
  ping: function() {
62
111
  if (this.disabled) return;
63
112
 
64
- this._client.publish('/terminus/ping', this.browserDetails());
65
- var self = this;
66
- setTimeout(function() { self.ping() }, 3000);
113
+ this.browserDetails(function(details) {
114
+ this._bayeux.publish('/terminus/ping', details);
115
+ var self = this;
116
+ setTimeout(function() { self.ping() }, 3000);
117
+ }, this);
118
+ },
119
+
120
+ handleMessage: function(message) {
121
+ var command = message.command,
122
+ method = command.shift(),
123
+ driver = this.Driver,
124
+ worker = this.Worker,
125
+ posted = false,
126
+ self = this;
127
+
128
+ command.push(function(result) {
129
+ if (posted) return;
130
+ self.postResult(message.commandId, result);
131
+ posted = true;
132
+ });
133
+
134
+ worker.monitor = true;
135
+ driver[method].apply(driver, command);
136
+ worker.monitor = false;
67
137
  },
68
138
 
69
139
  postResult: function(commandId, result) {
70
140
  if (this.disabled || !commandId) return;
71
141
 
72
- this._client.publish('/terminus/results', {
73
- id: this.getId(),
74
- commandId: commandId,
75
- result: result
76
- });
142
+ if (this._socket)
143
+ return this._socket.send(JSON.stringify({value: result}));
144
+
145
+ this.getId(function(id) {
146
+ this._bayeux.publish('/terminus/results', {
147
+ id: id,
148
+ commandId: commandId,
149
+ result: result
150
+ });
151
+ }, this);
152
+ },
153
+
154
+ getAttribute: function(node, name) {
155
+ return Terminus.isIE ? (node.getAttributeNode(name) || {}).nodeValue || false
156
+ : node.getAttribute(name);
77
157
  },
78
158
 
79
159
  Driver: {
@@ -83,15 +163,18 @@ Terminus = {
83
163
 
84
164
  attribute: function(nodeId, name, callback) {
85
165
  var node = this._node(nodeId);
166
+ if (!node) return callback(null);
86
167
 
87
- if (name === 'checked' || name === 'selected')
88
- return callback(!!node[name]);
89
-
90
- callback(Terminus.getAttribute(node, name));
168
+ if (!Terminus.isIE && (name === 'checked' || name === 'selected')) {
169
+ callback(!!node[name]);
170
+ } else {
171
+ callback(Terminus.getAttribute(node, name));
172
+ }
91
173
  },
92
174
 
93
175
  set_attribute: function(nodeId, name, value, callback) {
94
176
  var node = this._node(nodeId);
177
+ if (!node) return callback(null);
95
178
  node.setAttribute(name, value);
96
179
  callback(true);
97
180
  },
@@ -112,29 +195,44 @@ Terminus = {
112
195
  name = cookies[i].split('=')[0];
113
196
  document.cookie = name + '=; expires=' + expiry.toGMTString() + '; path=/';
114
197
  }
115
- callback();
198
+ callback(true);
116
199
  },
117
200
 
118
201
  click: function(nodeId, options, callback) {
119
202
  var element = this._node(nodeId),
120
203
  timeout = options.resynchronization_timeout;
121
204
 
205
+ if (!element) return callback(true);
206
+
122
207
  Syn.trigger('click', {}, element);
123
208
 
124
- if (options.resynchronize === false) return callback();
209
+ if (options.resynchronize === false) return callback(true);
125
210
 
126
211
  if (timeout)
127
212
  Terminus.Worker._setTimeout.call(window, function() {
128
213
  callback('failed to resynchronize, ajax request timed out');
129
214
  }, 1000 * timeout);
130
215
 
131
- Terminus.Worker.callback(callback);
216
+ Terminus.Worker.callback(function() {
217
+ callback(true);
218
+ });
219
+ },
220
+
221
+ current_url: function(callback) {
222
+ Terminus.browserDetails(function(details) {
223
+ callback(details.url);
224
+ });
132
225
  },
133
226
 
134
227
  drag: function(options, callback) {
135
228
  var draggable = this._node(options.from),
136
229
  droppable = this._node(options.to);
137
- Syn.drag({to: droppable}, draggable, callback);
230
+
231
+ if (!draggable || !droppable) return callback(null);
232
+
233
+ Syn.drag({to: droppable}, draggable, function() {
234
+ callback(true);
235
+ });
138
236
  },
139
237
 
140
238
  evaluate: function(expression, callback) {
@@ -143,11 +241,12 @@ Terminus = {
143
241
 
144
242
  execute: function(expression, callback) {
145
243
  eval(expression);
146
- callback();
244
+ callback(true);
147
245
  },
148
246
 
149
247
  find: function(xpath, nodeId, callback) {
150
- var root = this._node(nodeId) || document;
248
+ var root = nodeId ? this._node(nodeId) : document;
249
+ if (!root) return callback([]);
151
250
 
152
251
  var result = document.evaluate(xpath, root, null, XPathResult.ANY_TYPE, null),
153
252
  list = [],
@@ -159,13 +258,9 @@ Terminus = {
159
258
  return callback(list);
160
259
  },
161
260
 
162
- frame_src: function(name, callback) {
163
- var frame = document.getElementById(name);
164
- callback(frame.src);
165
- },
166
-
167
261
  is_visible: function(nodeId, callback) {
168
262
  var node = this._node(nodeId);
263
+ if (!node) return callback(null);
169
264
 
170
265
  while (node.tagName && node.tagName.toLowerCase() !== 'body') {
171
266
  if (node.style.display === 'none' || node.type === 'hidden')
@@ -177,6 +272,7 @@ Terminus = {
177
272
 
178
273
  select: function(nodeId, callback) {
179
274
  var option = this._node(nodeId);
275
+ if (!option) return callback(null);
180
276
  option.selected = true;
181
277
  Syn.trigger('change', {}, option.parentNode);
182
278
  callback(true);
@@ -186,6 +282,7 @@ Terminus = {
186
282
  var field = this._node(nodeId),
187
283
  max = Terminus.getAttribute(field, 'maxlength');
188
284
 
285
+ if (!field) return callback(null);
189
286
  if (field.type === 'file') return callback('not_allowed');
190
287
 
191
288
  Syn.trigger('focus', {}, field);
@@ -201,16 +298,20 @@ Terminus = {
201
298
  break;
202
299
  }
203
300
  Syn.trigger('change', {}, field);
204
- callback();
301
+ callback(true);
205
302
  },
206
303
 
207
304
  tag_name: function(nodeId, callback) {
208
- callback(this._node(nodeId).tagName.toLowerCase());
305
+ var node = this._node(nodeId);
306
+ if (!node) return callback(null);
307
+ callback(node.tagName.toLowerCase());
209
308
  },
210
309
 
211
310
  text: function(nodeId, callback) {
212
- var node = this._node(nodeId),
213
- text = node.textContent || node.innerText || '',
311
+ var node = this._node(nodeId);
312
+ if (!node) return callback(null);
313
+
314
+ var text = node.textContent || node.innerText || '',
214
315
  scripts = node.getElementsByTagName('script'),
215
316
  i = scripts.length;
216
317
 
@@ -221,12 +322,14 @@ Terminus = {
221
322
 
222
323
  trigger: function(nodeId, eventType, callback) {
223
324
  var node = this._node(nodeId);
325
+ if (!node) return callback(null);
224
326
  Syn.trigger(eventType, {}, node);
225
- callback();
327
+ callback(true);
226
328
  },
227
329
 
228
330
  unselect: function(nodeId, callback) {
229
331
  var option = this._node(nodeId);
332
+ if (!option) return callback(null);
230
333
  if (!option.parentNode.multiple) return callback(false);
231
334
  option.selected = false;
232
335
  Syn.trigger('change', {}, option.parentNode);
@@ -235,6 +338,8 @@ Terminus = {
235
338
 
236
339
  value: function(nodeId, callback) {
237
340
  var node = this._node(nodeId);
341
+ if (!node) return callback(null);
342
+
238
343
  if (node.tagName.toLowerCase() !== 'select' || !node.multiple)
239
344
  return callback(node.value);
240
345
 
@@ -249,7 +354,7 @@ Terminus = {
249
354
 
250
355
  visit: function(url, callback) {
251
356
  window.location.href = url;
252
- callback();
357
+ callback(url);
253
358
  }
254
359
  },
255
360
 
@@ -260,7 +365,11 @@ Terminus = {
260
365
  },
261
366
 
262
367
  get: function(id) {
263
- return this._elements[id];
368
+ var node = this._elements[id], root = node;
369
+ while (root && root.tagName !== 'BODY' && root.tagName !== 'HTML')
370
+ root = root.parentNode;
371
+ if (!root) return null;
372
+ return node;
264
373
  },
265
374
 
266
375
  put: function(element) {
@@ -275,14 +384,18 @@ Terminus = {
275
384
  this._callbacks = [];
276
385
  this._pending = 0;
277
386
 
278
- this._wrapTimeouts();
387
+ if (!Terminus.isIE) this._wrapTimeouts();
279
388
  },
280
389
 
281
390
  callback: function(callback, scope) {
282
- if (this._pending === 0)
283
- this._setTimeout.call(window, function() { callback.call(scope) }, 0);
284
- else
391
+ if (this._pending === 0) {
392
+ if (this._setTimeout)
393
+ this._setTimeout.call(window, function() { callback.call(scope) }, 0);
394
+ else
395
+ setTimeout(function() { callback.call(scope) }, 0);
396
+ } else {
285
397
  this._callbacks.push([callback, scope]);
398
+ }
286
399
  },
287
400
 
288
401
  suspend: function() {
@@ -1,3 +1,9 @@
1
+ require 'rack'
2
+ require 'thin'
3
+
4
+ Faye::WebSocket.load_adapter('thin') if Faye::WebSocket.respond_to?(:load_adapter)
5
+ Thin::Logging.silent = true
6
+
1
7
  module Terminus
2
8
  class Server
3
9
 
@@ -8,12 +8,14 @@ module Terminus
8
8
  TIMEOUT = 30
9
9
 
10
10
  def wait_with_timeout(name, duration = TIMEOUT, &predicate)
11
- result, time_out = nil, false
11
+ result, time_out = predicate.call, false
12
+ return result if result
13
+
12
14
  add_timeout(name, duration) { time_out = true }
13
15
 
14
16
  while !result and !time_out
15
17
  result = predicate.call
16
- sleep 0.001
18
+ sleep(0.001)
17
19
  end
18
20
 
19
21
  raise TimeoutError.new("Waited #{duration}s but could not get a #{name}") if time_out
@@ -1,9 +1,7 @@
1
1
  (function() {
2
- var faye = '<%= ::Terminus::FAYE_MOUNT %>',
3
- host = '<%= host %>';
4
-
5
- JSCLASS_PATH = host + '/js.class/';
6
- SYN_FILES = ['synthetic', 'mouse', 'browsers', 'key', 'drag/drag'];
2
+ var faye = '<%= ::Terminus::FAYE_MOUNT %>',
3
+ host = '<%= env['SERVER_NAME'] %>',
4
+ origin = 'http://' + host + ':' + <%= Terminus.port %>;
7
5
 
8
6
  var withPackageManager = function(callback) {
9
7
  if (window.JS && JS.Packages) return callback();
@@ -12,13 +10,12 @@
12
10
  head = document.getElementsByTagName('head')[0];
13
11
 
14
12
  script.type = 'text/javascript';
15
- script.src = host + '/loader.js';
13
+ script.src = origin + '/loader.js';
16
14
 
17
15
  script.onload = script.onreadystatechange = function() {
18
16
  var state = script.readyState;
19
17
  if (!state || state === 'loaded' || state === 'complete') {
20
18
  script.onload = script.onreadystatechange = null;
21
- head.removeChild(script);
22
19
  callback();
23
20
  }
24
21
  };
@@ -26,29 +23,17 @@
26
23
  };
27
24
 
28
25
  withPackageManager(function() {
26
+ JS.cacheBust = true;
27
+
29
28
  JS.Packages(function() { with(this) {
30
- loader(function(callback) {
31
- load(host + faye + '/client.js', callback);
32
- }).provides('Faye', 'Faye.Client');
33
-
34
- loader(function(callback) {
35
- if (!window.steal) window.steal = {then: function(fn) { fn() }};
36
- var inject = function() {
37
- var synFile = SYN_FILES.shift();
38
- if (!synFile) return callback();
39
- load(host + '/syn/' + synFile + '.js', inject);
40
- };
41
- inject();
42
-
43
- }).provides('Syn');
44
-
45
- file(host + '/terminus.js')
46
- .requires('Faye.Client', 'document.evaluate', 'Syn')
47
- .provides('Terminus');
29
+ file(origin + faye + '/client.js').provides('Faye', 'Faye.Client');
30
+ file(origin + '/compiled/terminus-min.js').requires('Faye.Client').provides('Terminus');
48
31
  }});
49
32
 
50
- JS.require('Terminus', function() {
51
- Terminus.connect(host + faye);
33
+ JS.require('Faye.Client', function() {
34
+ JS.require('Terminus', function() {
35
+ Terminus.connect(host, <%= ::Terminus.port %>);
36
+ });
52
37
  });
53
38
  });
54
39
  })();