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,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
  })();