summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSolly Ross <sross@redhat.com>2014-05-22 16:57:55 -0400
committerSolly Ross <sross@redhat.com>2014-09-15 16:46:02 -0400
commit2cccf7530c272de3627fa871da8e6ed44f437edb (patch)
tree8b4b9f1f4667413620f8eee44e441d85b26b7782
parentee7d4c61c6c2b39bc257d9fc1c20d2b2a479d370 (diff)
downloadnovnc-2cccf7530c272de3627fa871da8e6ed44f437edb.tar.gz
Cleanup: WebSocket Helper
File: websock.js Tests Added: True Changes: - Cleaned up JSHint errors - Converted to normal JS constructor pattern with "private" fields and methods now simply being prepended by underscores - Added a "bind" polyfill for use in PhantomJS 1.x in util.js - Added FakeWebSocket to fill in for actual WebSocket objects when testing - Made exception handler actually log exception name and message, to console, in addition to stack trace
-rw-r--r--include/websock.js650
-rw-r--r--tests/fake.websocket.js96
-rw-r--r--tests/test.websock.js480
3 files changed, 877 insertions, 349 deletions
diff --git a/include/websock.js b/include/websock.js
index 0e4718a..bd3179a 100644
--- a/include/websock.js
+++ b/include/websock.js
@@ -14,7 +14,7 @@
* read binary data off of the receive queue.
*/
-/*jslint browser: true, bitwise: false, plusplus: false */
+/*jslint browser: true, bitwise: true */
/*global Util, Base64 */
@@ -43,382 +43,334 @@ if (window.WebSocket && !window.WEB_SOCKET_FORCE_FLASH) {
}
Util.load_scripts(["web-socket-js/swfobject.js",
"web-socket-js/web_socket.js"]);
- }());
+ })();
}
function Websock() {
-"use strict";
-
-var api = {}, // Public API
- websocket = null, // WebSocket object
- mode = 'base64', // Current WebSocket mode: 'binary', 'base64'
- rQ = [], // Receive queue
- rQi = 0, // Receive queue index
- rQmax = 10000, // Max receive queue size before compacting
- sQ = [], // Send queue
-
- eventHandlers = {
- 'message' : function() {},
- 'open' : function() {},
- 'close' : function() {},
- 'error' : function() {}
- },
-
- test_mode = false;
-
-
-//
-// Queue public functions
-//
-
-function get_sQ() {
- return sQ;
+ "use strict";
+
+ this._websocket = null; // WebSocket object
+ this._rQ = []; // Receive queue
+ this._rQi = 0; // Receive queue index
+ this._rQmax = 10000; // Max receive queue size before compacting
+ this._sQ = []; // Send queue
+
+ this._mode = 'base64'; // Current WebSocket mode: 'binary', 'base64'
+ this.maxBufferedAmount = 200;
+
+ this._eventHandlers = {
+ 'message': function () {},
+ 'open': function () {},
+ 'close': function () {},
+ 'error': function () {}
+ };
}
-function get_rQ() {
- return rQ;
-}
-function get_rQi() {
- return rQi;
-}
-function set_rQi(val) {
- rQi = val;
-}
+(function () {
+ "use strict";
+ Websock.prototype = {
+ // Getters and Setters
+ get_sQ: function () {
+ return this._sQ;
+ },
+
+ get_rQ: function () {
+ return this._rQ;
+ },
+
+ get_rQi: function () {
+ return this._rQi;
+ },
+
+ set_rQi: function (val) {
+ this._rQi = val;
+ },
+
+ // Receive Queue
+ rQlen: function () {
+ return this._rQ.length - this._rQi;
+ },
+
+ rQpeek8: function () {
+ return this._rQ[this._rQi];
+ },
+
+ rQshift8: function () {
+ return this._rQ[this._rQi++];
+ },
+
+ rQunshift8: function (num) {
+ if (this._rQi === 0) {
+ this._rQ.unshift(num);
+ } else {
+ this._rQi--;
+ this._rQ[this._rQi] = num;
+ }
+ },
+
+ rQshift16: function () {
+ return (this._rQ[this._rQi++] << 8) +
+ this._rQ[this._rQi++];
+ },
+
+ rQshift32: function () {
+ return (this._rQ[this._rQi++] << 24) +
+ (this._rQ[this._rQi++] << 16) +
+ (this._rQ[this._rQi++] << 8) +
+ this._rQ[this._rQi++];
+ },
+
+ rQshiftStr: function (len) {
+ if (typeof(len) === 'undefined') { len = this.rQlen(); }
+ var arr = this._rQ.slice(this._rQi, this._rQi + len);
+ this._rQi += len;
+ return String.fromCharCode.apply(null, arr);
+ },
+
+ rQshiftBytes: function (len) {
+ if (typeof(len) === 'undefined') { len = this.rQlen(); }
+ this._rQi += len;
+ return this._rQ.slice(this._rQi - len, this._rQi);
+ },
+
+ rQslice: function (start, end) {
+ if (end) {
+ return this._rQ.slice(this._rQi + start, this._rQi + end);
+ } else {
+ return this._rQ.slice(this._rQi + start);
+ }
+ },
+
+ // Check to see if we must wait for 'num' bytes (default to FBU.bytes)
+ // to be available in the receive queue. Return true if we need to
+ // wait (and possibly print a debug message), otherwise false.
+ rQwait: function (msg, num, goback) {
+ var rQlen = this._rQ.length - this._rQi; // Skip rQlen() function call
+ if (rQlen < num) {
+ if (goback) {
+ if (this._rQi < goback) {
+ throw new Error("rQwait cannot backup " + goback + " bytes");
+ }
+ this._rQi -= goback;
+ }
+ return true; // true means need more data
+ }
+ return false;
+ },
-function rQlen() {
- return rQ.length - rQi;
-}
+ // Send Queue
-function rQpeek8() {
- return (rQ[rQi] );
-}
-function rQshift8() {
- return (rQ[rQi++] );
-}
-function rQunshift8(num) {
- if (rQi === 0) {
- rQ.unshift(num);
- } else {
- rQi -= 1;
- rQ[rQi] = num;
- }
+ flush: function () {
+ if (this._websocket.bufferedAmount !== 0) {
+ Util.Debug("bufferedAmount: " + this._websocket.bufferedAmount);
+ }
-}
-function rQshift16() {
- return (rQ[rQi++] << 8) +
- (rQ[rQi++] );
-}
-function rQshift32() {
- return (rQ[rQi++] << 24) +
- (rQ[rQi++] << 16) +
- (rQ[rQi++] << 8) +
- (rQ[rQi++] );
-}
-function rQshiftStr(len) {
- if (typeof(len) === 'undefined') { len = rQlen(); }
- var arr = rQ.slice(rQi, rQi + len);
- rQi += len;
- return String.fromCharCode.apply(null, arr);
-}
-function rQshiftBytes(len) {
- if (typeof(len) === 'undefined') { len = rQlen(); }
- rQi += len;
- return rQ.slice(rQi-len, rQi);
-}
+ if (this._websocket.bufferedAmount < this.maxBufferedAmount) {
+ if (this._sQ.length > 0) {
+ this._websocket.send(this._encode_message());
+ this._sQ = [];
+ }
-function rQslice(start, end) {
- if (end) {
- return rQ.slice(rQi + start, rQi + end);
- } else {
- return rQ.slice(rQi + start);
- }
-}
+ return true;
+ } else {
+ Util.Info("Delaying send, bufferedAmount: " +
+ this._websocket.bufferedAmount);
+ return false;
+ }
+ },
+
+ send: function (arr) {
+ this._sQ = this._sQ.concat(arr);
+ return this.flush();
+ },
+
+ send_string: function (str) {
+ this.send(str.split('').map(function (chr) {
+ return chr.charCodeAt(0);
+ }));
+ },
+
+ // Event Handlers
+ on: function (evt, handler) {
+ this._eventHandlers[evt] = handler;
+ },
+
+ init: function (protocols, ws_schema) {
+ this._rQ = [];
+ this._rQi = 0;
+ this._sQ = [];
+ this._websocket = null;
+
+ // Check for full typed array support
+ var bt = false;
+ if (('Uint8Array' in window) &&
+ ('set' in Uint8Array.prototype)) {
+ bt = true;
+ }
-// Check to see if we must wait for 'num' bytes (default to FBU.bytes)
-// to be available in the receive queue. Return true if we need to
-// wait (and possibly print a debug message), otherwise false.
-function rQwait(msg, num, goback) {
- var rQlen = rQ.length - rQi; // Skip rQlen() function call
- if (rQlen < num) {
- if (goback) {
- if (rQi < goback) {
- throw("rQwait cannot backup " + goback + " bytes");
+ // Check for full binary type support in WebSockets
+ // Inspired by:
+ // https://github.com/Modernizr/Modernizr/issues/370
+ // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/websockets/binary.js
+ var wsbt = false;
+ try {
+ if (bt && ('binaryType' in WebSocket.prototype ||
+ !!(new WebSocket(ws_schema + '://.').binaryType))) {
+ Util.Info("Detected binaryType support in WebSockets");
+ wsbt = true;
+ }
+ } catch (exc) {
+ // Just ignore failed test localhost connection
}
- rQi -= goback;
- }
- //Util.Debug(" waiting for " + (num-rQlen) +
- // " " + msg + " byte(s)");
- return true; // true means need more data
- }
- return false;
-}
-//
-// Private utility routines
-//
-
-function encode_message() {
- if (mode === 'binary') {
- // Put in a binary arraybuffer
- return (new Uint8Array(sQ)).buffer;
- } else {
- // base64 encode
- return Base64.encode(sQ);
- }
-}
+ // Default protocols if not specified
+ if (typeof(protocols) === "undefined") {
+ if (wsbt) {
+ protocols = ['binary', 'base64'];
+ } else {
+ protocols = 'base64';
+ }
+ }
-function decode_message(data) {
- //Util.Debug(">> decode_message: " + data);
- if (mode === 'binary') {
- // push arraybuffer values onto the end
- var u8 = new Uint8Array(data);
- for (var i = 0; i < u8.length; i++) {
- rQ.push(u8[i]);
- }
- } else {
- // base64 decode and concat to the end
- rQ = rQ.concat(Base64.decode(data, 0));
- }
- //Util.Debug(">> decode_message, rQ: " + rQ);
-}
+ if (!wsbt) {
+ if (protocols === 'binary') {
+ throw new Error('WebSocket binary sub-protocol requested but not supported');
+ }
+ if (typeof(protocols) === 'object') {
+ var new_protocols = [];
+
+ for (var i = 0; i < protocols.length; i++) {
+ if (protocols[i] === 'binary') {
+ Util.Error('Skipping unsupported WebSocket binary sub-protocol');
+ } else {
+ new_protocols.push(protocols[i]);
+ }
+ }
+
+ if (new_protocols.length > 0) {
+ protocols = new_protocols;
+ } else {
+ throw new Error("Only WebSocket binary sub-protocol was requested and is not supported.");
+ }
+ }
+ }
-//
-// Public Send functions
-//
-
-function flush() {
- if (websocket.bufferedAmount !== 0) {
- Util.Debug("bufferedAmount: " + websocket.bufferedAmount);
- }
- if (websocket.bufferedAmount < api.maxBufferedAmount) {
- //Util.Debug("arr: " + arr);
- //Util.Debug("sQ: " + sQ);
- if (sQ.length > 0) {
- websocket.send(encode_message(sQ));
- sQ = [];
- }
- return true;
- } else {
- Util.Info("Delaying send, bufferedAmount: " +
- websocket.bufferedAmount);
- return false;
- }
-}
+ return protocols;
+ },
-// overridable for testing
-function send(arr) {
- //Util.Debug(">> send_array: " + arr);
- sQ = sQ.concat(arr);
- return flush();
-}
+ open: function (uri, protocols) {
+ var ws_schema = uri.match(/^([a-z]+):\/\//)[1];
+ protocols = this.init(protocols, ws_schema);
-function send_string(str) {
- //Util.Debug(">> send_string: " + str);
- api.send(str.split('').map(
- function (chr) { return chr.charCodeAt(0); } ) );
-}
+ this._websocket = new WebSocket(uri, protocols);
-//
-// Other public functions
-
-function recv_message(e) {
- //Util.Debug(">> recv_message: " + e.data.length);
-
- try {
- decode_message(e.data);
- if (rQlen() > 0) {
- eventHandlers.message();
- // Compact the receive queue
- if (rQ.length > rQmax) {
- //Util.Debug("Compacting receive queue");
- rQ = rQ.slice(rQi);
- rQi = 0;
+ if (protocols.indexOf('binary') >= 0) {
+ this._websocket.binaryType = 'arraybuffer';
}
- } else {
- Util.Debug("Ignoring empty message");
- }
- } catch (exc) {
- if (typeof exc.stack !== 'undefined') {
- Util.Warn("recv_message, caught exception: " + exc.stack);
- } else if (typeof exc.description !== 'undefined') {
- Util.Warn("recv_message, caught exception: " + exc.description);
- } else {
- Util.Warn("recv_message, caught exception:" + exc);
- }
- if (typeof exc.name !== 'undefined') {
- eventHandlers.error(exc.name + ": " + exc.message);
- } else {
- eventHandlers.error(exc);
- }
- }
- //Util.Debug("<< recv_message");
-}
-
-// Set event handlers
-function on(evt, handler) {
- eventHandlers[evt] = handler;
-}
-
-function init(protocols, ws_schema) {
- rQ = [];
- rQi = 0;
- sQ = [];
- websocket = null;
-
- var bt = false,
- wsbt = false,
- try_binary = false;
-
- // Check for full typed array support
- if (('Uint8Array' in window) &&
- ('set' in Uint8Array.prototype)) {
- bt = true;
- }
- // Check for full binary type support in WebSocket
- // Inspired by:
- // https://github.com/Modernizr/Modernizr/issues/370
- // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/websockets/binary.js
- try {
- if (bt && ('binaryType' in WebSocket.prototype ||
- !!(new WebSocket(ws_schema + '://.').binaryType))) {
- Util.Info("Detected binaryType support in WebSockets");
- wsbt = true;
- }
- } catch (exc) {
- // Just ignore failed test localhost connections
- }
-
- // Default protocols if not specified
- if (typeof(protocols) === "undefined") {
- if (wsbt) {
- protocols = ['binary', 'base64'];
- } else {
- protocols = 'base64';
- }
- }
-
- // If no binary support, make sure it was not requested
- if (!wsbt) {
- if (protocols === 'binary') {
- throw("WebSocket binary sub-protocol requested but not supported");
- }
- if (typeof(protocols) === "object") {
- var new_protocols = [];
- for (var i = 0; i < protocols.length; i++) {
- if (protocols[i] === 'binary') {
- Util.Error("Skipping unsupported WebSocket binary sub-protocol");
+ this._websocket.onmessage = this._recv_message.bind(this);
+ this._websocket.onopen = (function () {
+ Util.Debug('>> WebSock.onopen');
+ if (this._websocket.protocol) {
+ this._mode = this._websocket.protocol;
+ Util.Info("Server choose sub-protocol: " + this._websocket.protocol);
} else {
- new_protocols.push(protocols[i]);
+ this._mode = 'base64';
+ Util.Error('Server select no sub-protocol!: ' + this._websocket.protocol);
}
+ this._eventHandlers.open();
+ Util.Debug("<< WebSock.onopen");
+ }).bind(this);
+ this._websocket.onclose = (function (e) {
+ Util.Debug(">> WebSock.onclose");
+ this._eventHandlers.close(e);
+ Util.Debug("<< WebSock.onclose");
+ }).bind(this);
+ this._websocket.onerror = (function (e) {
+ Util.Debug(">> WebSock.onerror: " + e);
+ this._eventHandlers.error(e);
+ Util.Debug("<< WebSock.onerror: " + e);
+ }).bind(this);
+ },
+
+ close: function () {
+ if (this._websocket) {
+ if ((this._websocket.readyState === WebSocket.OPEN) ||
+ (this._websocket.readyState === WebSocket.CONNECTING)) {
+ Util.Info("Closing WebSocket connection");
+ this._websocket.close();
+ }
+
+ this._websocket.onmessage = function (e) { return; };
}
- if (new_protocols.length > 0) {
- protocols = new_protocols;
+ },
+
+ // private methods
+ _encode_message: function () {
+ if (this._mode === 'binary') {
+ // Put in a binary arraybuffer
+ return (new Uint8Array(this._sQ)).buffer;
} else {
- throw("Only WebSocket binary sub-protocol was requested and not supported.");
+ // base64 encode
+ return Base64.encode(this._sQ);
}
- }
- }
+ },
+
+ _decode_message: function (data) {
+ if (this._mode === 'binary') {
+ // push arraybuffer values onto the end
+ var u8 = new Uint8Array(data);
+ for (var i = 0; i < u8.length; i++) {
+ this._rQ.push(u8[i]);
+ }
+ } else {
+ // base64 decode and concat to end
+ this._rQ = this._rQ.concat(Base64.decode(data, 0));
+ }
+ },
+
+ _recv_message: function (e) {
+ try {
+ this._decode_message(e.data);
+ if (this.rQlen() > 0) {
+ this._eventHandlers.message();
+ // Compact the receive queue
+ if (this._rQ.length > this._rQmax) {
+ this._rQ = this._rQ.slice(this._rQi);
+ this._rQi = 0;
+ }
+ } else {
+ Util.Debug("Ignoring empty message");
+ }
+ } catch (exc) {
+ var exception_str = "";
+ if (exc.name) {
+ exception_str += "\n name: " + exc.name + "\n";
+ exception_str += " message: " + exc.message + "\n";
+ }
- return protocols;
-}
+ if (typeof exc.description !== 'undefined') {
+ exception_str += " description: " + exc.description + "\n";
+ }
-function open(uri, protocols) {
- var ws_schema = uri.match(/^([a-z]+):\/\//)[1];
- protocols = init(protocols, ws_schema);
+ if (typeof exc.stack !== 'undefined') {
+ exception_str += exc.stack;
+ }
- if (test_mode) {
- websocket = {};
- } else {
- websocket = new WebSocket(uri, protocols);
- if (protocols.indexOf('binary') >= 0) {
- websocket.binaryType = 'arraybuffer';
- }
- }
-
- websocket.onmessage = recv_message;
- websocket.onopen = function() {
- Util.Debug(">> WebSock.onopen");
- if (websocket.protocol) {
- mode = websocket.protocol;
- Util.Info("Server chose sub-protocol: " + websocket.protocol);
- } else {
- mode = 'base64';
- Util.Error("Server select no sub-protocol!: " + websocket.protocol);
- }
- eventHandlers.open();
- Util.Debug("<< WebSock.onopen");
- };
- websocket.onclose = function(e) {
- Util.Debug(">> WebSock.onclose");
- eventHandlers.close(e);
- Util.Debug("<< WebSock.onclose");
- };
- websocket.onerror = function(e) {
- Util.Debug(">> WebSock.onerror: " + e);
- eventHandlers.error(e);
- Util.Debug("<< WebSock.onerror");
- };
-}
+ if (exception_str.length > 0) {
+ Util.Error("recv_message, caught exception: " + exception_str);
+ } else {
+ Util.Error("recv_message, caught exception: " + exc);
+ }
-function close() {
- if (websocket) {
- if ((websocket.readyState === WebSocket.OPEN) ||
- (websocket.readyState === WebSocket.CONNECTING)) {
- Util.Info("Closing WebSocket connection");
- websocket.close();
+ if (typeof exc.name !== 'undefined') {
+ this._eventHandlers.error(exc.name + ": " + exc.message);
+ } else {
+ this._eventHandlers.error(exc);
+ }
+ }
}
- websocket.onmessage = function (e) { return; };
- }
-}
-
-// Override internal functions for testing
-// Takes a send function, returns reference to recv function
-function testMode(override_send, data_mode) {
- test_mode = true;
- mode = data_mode;
- api.send = override_send;
- api.close = function () {};
- return recv_message;
-}
-
-function constructor() {
- // Configuration settings
- api.maxBufferedAmount = 200;
-
- // Direct access to send and receive queues
- api.get_sQ = get_sQ;
- api.get_rQ = get_rQ;
- api.get_rQi = get_rQi;
- api.set_rQi = set_rQi;
-
- // Routines to read from the receive queue
- api.rQlen = rQlen;
- api.rQpeek8 = rQpeek8;
- api.rQshift8 = rQshift8;
- api.rQunshift8 = rQunshift8;
- api.rQshift16 = rQshift16;
- api.rQshift32 = rQshift32;
- api.rQshiftStr = rQshiftStr;
- api.rQshiftBytes = rQshiftBytes;
- api.rQslice = rQslice;
- api.rQwait = rQwait;
-
- api.flush = flush;
- api.send = send;
- api.send_string = send_string;
-
- api.on = on;
- api.init = init;
- api.open = open;
- api.close = close;
- api.testMode = testMode;
-
- return api;
-}
-
-return constructor();
-
-}
+ };
+})();
diff --git a/tests/fake.websocket.js b/tests/fake.websocket.js
new file mode 100644
index 0000000..749c0ea
--- /dev/null
+++ b/tests/fake.websocket.js
@@ -0,0 +1,96 @@
+var FakeWebSocket;
+
+(function () {
+ // PhantomJS can't create Event objects directly, so we need to use this
+ function make_event(name, props) {
+ var evt = document.createEvent('Event');
+ evt.initEvent(name, true, true);
+ if (props) {
+ for (var prop in props) {
+ evt[prop] = props[prop];
+ }
+ }
+ return evt;
+ }
+
+ FakeWebSocket = function (uri, protocols) {
+ this.url = uri;
+ this.binaryType = "arraybuffer";
+ this.extensions = "";
+
+ if (!protocols || typeof protocols === 'string') {
+ this.protocol = protocols;
+ } else {
+ this.protocol = protocols[0];
+ }
+
+ this._send_queue = new Uint8Array(20000);
+
+ this.readyState = FakeWebSocket.CONNECTING;
+ this.bufferedAmount = 0;
+
+ this.__is_fake = true;
+ };
+
+ FakeWebSocket.prototype = {
+ close: function (code, reason) {
+ this.readyState = FakeWebSocket.CLOSED;
+ if (this.onclose) {
+ this.onclose(make_event("close", { 'code': code, 'reason': reason, 'wasClean': true }));
+ }
+ },
+
+ send: function (data) {
+ if (this.protocol == 'base64') {
+ data = Base64.decode(data);
+ } else {
+ data = new Uint8Array(data);
+ }
+ this._send_queue.set(data, this.bufferedAmount);
+ this.bufferedAmount += data.length;
+ },
+
+ _get_sent_data: function () {
+ var arr = [];
+ for (var i = 0; i < this.bufferedAmount; i++) {
+ arr[i] = this._send_queue[i];
+ }
+
+ this.bufferedAmount = 0;
+
+ return arr;
+ },
+
+ _open: function (data) {
+ this.readyState = FakeWebSocket.OPEN;
+ if (this.onopen) {
+ this.onopen(make_event('open'));
+ }
+ },
+
+ _receive_data: function (data) {
+ this.onmessage(make_event("message", { 'data': data }));
+ }
+ };
+
+ FakeWebSocket.OPEN = WebSocket.OPEN;
+ FakeWebSocket.CONNECTING = WebSocket.CONNECTING;
+ FakeWebSocket.CLOSING = WebSocket.CLOSING;
+ FakeWebSocket.CLOSED = WebSocket.CLOSED;
+
+ FakeWebSocket.__is_fake = true;
+
+ FakeWebSocket.replace = function () {
+ if (!WebSocket.__is_fake) {
+ var real_version = WebSocket;
+ WebSocket = FakeWebSocket;
+ FakeWebSocket.__real_version = real_version;
+ }
+ };
+
+ FakeWebSocket.restore = function () {
+ if (WebSocket.__is_fake) {
+ WebSocket = WebSocket.__real_version;
+ }
+ };
+})();
diff --git a/tests/test.websock.js b/tests/test.websock.js
new file mode 100644
index 0000000..7d242d3
--- /dev/null
+++ b/tests/test.websock.js
@@ -0,0 +1,480 @@
+// requires local modules: websock, base64, util
+// requires test modules: fake.websocket
+/* jshint expr: true */
+var assert = chai.assert;
+var expect = chai.expect;
+
+describe('Websock', function() {
+ "use strict";
+
+ describe('Queue methods', function () {
+ var sock;
+ var RQ_TEMPLATE = [0, 1, 2, 3, 4, 5, 6, 7];
+
+ beforeEach(function () {
+ sock = new Websock();
+ for (var i = RQ_TEMPLATE.length - 1; i >= 0; i--) {
+ sock.rQunshift8(RQ_TEMPLATE[i]);
+ }
+ });
+ describe('rQlen', function () {
+ it('should return the length of the receive queue', function () {
+ sock.set_rQi(0);
+
+ expect(sock.rQlen()).to.equal(RQ_TEMPLATE.length);
+ });
+
+ it("should return the proper length if we read some from the receive queue", function () {
+ sock.set_rQi(1);
+
+ expect(sock.rQlen()).to.equal(RQ_TEMPLATE.length - 1);
+ });
+ });
+
+ describe('rQpeek8', function () {
+ it('should peek at the next byte without poping it off the queue', function () {
+ var bef_len = sock.rQlen();
+ var peek = sock.rQpeek8();
+ expect(sock.rQpeek8()).to.equal(peek);
+ expect(sock.rQlen()).to.equal(bef_len);
+ });
+ });
+
+ describe('rQshift8', function () {
+ it('should pop a single byte from the receive queue', function () {
+ var peek = sock.rQpeek8();
+ var bef_len = sock.rQlen();
+ expect(sock.rQshift8()).to.equal(peek);
+ expect(sock.rQlen()).to.equal(bef_len - 1);
+ });
+ });
+
+ describe('rQunshift8', function () {
+ it('should place a byte at the front of the queue', function () {
+ sock.rQunshift8(255);
+ expect(sock.rQpeek8()).to.equal(255);
+ expect(sock.rQlen()).to.equal(RQ_TEMPLATE.length + 1);
+ });
+ });
+
+ describe('rQshift16', function () {
+ it('should pop two bytes from the receive queue and return a single number', function () {
+ var bef_len = sock.rQlen();
+ var expected = (RQ_TEMPLATE[0] << 8) + RQ_TEMPLATE[1];
+ expect(sock.rQshift16()).to.equal(expected);
+ expect(sock.rQlen()).to.equal(bef_len - 2);
+ });
+ });
+
+ describe('rQshift32', function () {
+ it('should pop four bytes from the receive queue and return a single number', function () {
+ var bef_len = sock.rQlen();
+ var expected = (RQ_TEMPLATE[0] << 24) +
+ (RQ_TEMPLATE[1] << 16) +
+ (RQ_TEMPLATE[2] << 8) +
+ RQ_TEMPLATE[3];
+ expect(sock.rQshift32()).to.equal(expected);
+ expect(sock.rQlen()).to.equal(bef_len - 4);
+ });
+ });
+
+ describe('rQshiftStr', function () {
+ it('should shift the given number of bytes off of the receive queue and return a string', function () {
+ var bef_len = sock.rQlen();
+ var bef_rQi = sock.get_rQi();
+ var shifted = sock.rQshiftStr(3);
+ expect(shifted).to.be.a('string');
+ expect(shifted).to.equal(String.fromCharCode.apply(null, RQ_TEMPLATE.slice(bef_rQi, bef_rQi + 3)));
+ expect(sock.rQlen()).to.equal(bef_len - 3);
+ });
+
+ it('should shift the entire rest of the queue off if no length is given', function () {
+ sock.rQshiftStr();
+ expect(sock.rQlen()).to.equal(0);
+ });
+ });
+
+ describe('rQshiftBytes', function () {
+ it('should shift the given number of bytes of the receive queue and return an array', function () {
+ var bef_len = sock.rQlen();
+ var bef_rQi = sock.get_rQi();
+ var shifted = sock.rQshiftBytes(3);
+ expect(shifted).to.be.an.instanceof(Array);
+ expect(shifted).to.deep.equal(RQ_TEMPLATE.slice(bef_rQi, bef_rQi + 3));
+ expect(sock.rQlen()).to.equal(bef_len - 3);
+ });
+
+ it('should shift the entire rest of the queue off if no length is given', function () {
+ sock.rQshiftBytes();
+ expect(sock.rQlen()).to.equal(0);
+ });
+ });
+
+ describe('rQslice', function () {
+ beforeEach(function () {
+ sock.set_rQi(0);
+ });
+
+ it('should not modify the receive queue', function () {
+ var bef_len = sock.rQlen();
+ sock.rQslice(0, 2);
+ expect(sock.rQlen()).to.equal(bef_len);
+ });
+
+ it('should return an array containing the given slice of the receive queue', function () {
+ var sl = sock.rQslice(0, 2);
+ expect(sl).to.be.an.instanceof(Array);
+ expect(sl).to.deep.equal(RQ_TEMPLATE.slice(0, 2));
+ });
+
+ it('should use the rest of the receive queue if no end is given', function () {
+ var sl = sock.rQslice(1);
+ expect(sl).to.have.length(RQ_TEMPLATE.length - 1);
+ expect(sl).to.deep.equal(RQ_TEMPLATE.slice(1));
+ });
+
+ it('should take the current rQi in to account', function () {
+ sock.set_rQi(1);
+ expect(sock.rQslice(0, 2)).to.deep.equal(RQ_TEMPLATE.slice(1, 3));
+ });
+ });
+
+ describe('rQwait', function () {
+ beforeEach(function () {
+ sock.set_rQi(0);
+ });
+
+ it('should return true if there are not enough bytes in the receive queue', function () {
+ expect(sock.rQwait('hi', RQ_TEMPLATE.length + 1)).to.be.true;
+ });
+
+ it('should return false if there are enough bytes in the receive queue', function () {
+ expect(sock.rQwait('hi', RQ_TEMPLATE.length)).to.be.false;
+ });
+
+ it('should return true and reduce rQi by "goback" if there are not enough bytes', function () {
+ sock.set_rQi(5);
+ expect(sock.rQwait('hi', RQ_TEMPLATE.length, 4)).to.be.true;
+ expect(sock.get_rQi()).to.equal(1);
+ });
+
+ it('should raise an error if we try to go back more than possible', function () {
+ sock.set_rQi(5);
+ expect(function () { sock.rQwait('hi', RQ_TEMPLATE.length, 6); }).to.throw(Error);
+ });
+
+ it('should not reduce rQi if there are enough bytes', function () {
+ sock.set_rQi(5);
+ sock.rQwait('hi', 1, 6);
+ expect(sock.get_rQi()).to.equal(5);
+ });
+ });
+
+ describe('flush', function () {
+ beforeEach(function () {
+ sock._websocket = {
+ send: sinon.spy()
+ };
+ });
+
+ it('should actually send on the websocket if the websocket does not have too much buffered', function () {
+ sock.maxBufferedAmount = 10;
+ sock._websocket.bufferedAmount = 8;
+ sock._sQ = [1, 2, 3];
+ var encoded = sock._encode_message();
+
+ sock.flush();
+ expect(sock._websocket.send).to.have.been.calledOnce;
+ expect(sock._websocket.send).to.have.been.calledWith(encoded);
+ });
+
+ it('should return true if the websocket did not have too much buffered', function () {
+ sock.maxBufferedAmount = 10;
+ sock._websocket.bufferedAmount = 8;
+
+ expect(sock.flush()).to.be.true;
+ });
+
+ it('should not call send if we do not have anything queued up', function () {
+ sock._sQ = [];
+ sock.maxBufferedAmount = 10;
+ sock._websocket.bufferedAmount = 8;
+
+ sock.flush();
+
+ expect(sock._websocket.send).not.to.have.been.called;
+ });
+
+ it('should not send and return false if the websocket has too much buffered', function () {
+ sock.maxBufferedAmount = 10;
+ sock._websocket.bufferedAmount = 12;
+
+ expect(sock.flush()).to.be.false;
+ expect(sock._websocket.send).to.not.have.been.called;
+ });
+ });
+
+ describe('send', function () {
+ beforeEach(function () {
+ sock.flush = sinon.spy();
+ });
+
+ it('should add to the send queue', function () {
+ sock.send([1, 2, 3]);
+ var sq = sock.get_sQ();
+ expect(sock.get_sQ().slice(sq.length - 3)).to.deep.equal([1, 2, 3]);
+ });
+
+ it('should call flush', function () {
+ sock.send([1, 2, 3]);
+ expect(sock.flush).to.have.been.calledOnce;
+ });
+ });
+
+ describe('send_string', function () {
+ beforeEach(function () {
+ sock.send = sinon.spy();
+ });
+
+ it('should call send after converting the string to an array', function () {
+ sock.send_string("\x01\x02\x03");
+ expect(sock.send).to.have.been.calledWith([1, 2, 3]);
+ });
+ });
+ });
+
+ describe('lifecycle methods', function () {
+ var old_WS;
+ before(function () {
+ old_WS = WebSocket;
+ });
+
+ var sock;
+ beforeEach(function () {
+ sock = new Websock();
+ WebSocket = sinon.spy();
+ WebSocket.OPEN = old_WS.OPEN;
+ WebSocket.CONNECTING = old_WS.CONNECTING;
+ WebSocket.CLOSING = old_WS.CLOSING;
+ WebSocket.CLOSED = old_WS.CLOSED;
+ });
+
+ describe('opening', function () {
+ it('should pick the correct protocols if none are given' , function () {
+
+ });
+
+ it('should open the actual websocket', function () {
+ sock.open('ws://localhost:8675', 'base64');
+ expect(WebSocket).to.have.been.calledWith('ws://localhost:8675', 'base64');
+ });
+
+ it('should fail if we try to use binary but do not support it', function () {
+ expect(function () { sock.open('ws:///', 'binary'); }).to.throw(Error);
+ });
+
+ it('should fail if we specified an array with only binary and we do not support it', function () {
+ expect(function () { sock.open('ws:///', ['binary']); }).to.throw(Error);
+ });
+
+ it('should skip binary if we have multiple options for encoding and do not support binary', function () {
+ sock.open('ws:///', ['binary', 'base64']);
+ expect(WebSocket).to.have.been.calledWith('ws:///', ['base64']);
+ });
+ // it('should initialize the event handlers')?
+ });
+
+ describe('closing', function () {
+ beforeEach(function () {
+ sock.open('ws://');
+ sock._websocket.close = sinon.spy();
+ });
+
+ it('should close the actual websocket if it is open', function () {
+ sock._websocket.readyState = WebSocket.OPEN;
+ sock.close();
+ expect(sock._websocket.close).to.have.been.calledOnce;
+ });
+
+ it('should close the actual websocket if it is connecting', function () {
+ sock._websocket.readyState = WebSocket.CONNECTING;
+ sock.close();
+ expect(sock._websocket.close).to.have.been.calledOnce;
+ });
+
+ it('should not try to close the actual websocket if closing', function () {
+ sock._websocket.readyState = WebSocket.CLOSING;
+ sock.close();
+ expect(sock._websocket.close).not.to.have.been.called;
+ });
+
+ it('should not try to close the actual websocket if closed', function () {
+ sock._websocket.readyState = WebSocket.CLOSED;
+ sock.close();
+ expect(sock._websocket.close).not.to.have.been.called;
+ });
+
+ it('should reset onmessage to not call _recv_message', function () {
+ sinon.spy(sock, '_recv_message');
+ sock.close();
+ sock._websocket.onmessage(null);
+ try {
+ expect(sock._recv_message).not.to.have.been.called;
+ } finally {
+ sock._recv_message.restore();
+ }
+ });
+ });
+
+ describe('event handlers', function () {
+ beforeEach(function () {
+ sock._recv_message = sinon.spy();
+ sock.on('open', sinon.spy());
+ sock.on('close', sinon.spy());
+ sock.on('error', sinon.spy());
+ sock.open('ws://');
+ });
+
+ it('should call _recv_message on a message', function () {
+ sock._websocket.onmessage(null);
+ expect(sock._recv_message).to.have.been.calledOnce;
+ });
+
+ it('should copy the mode over upon opening', function () {
+ sock._websocket.protocol = 'cheese';
+ sock._websocket.onopen();
+ expect(sock._mode).to.equal('cheese');
+ });
+
+ it('should assume base64 if no protocol was available on opening', function () {
+ sock._websocket.protocol = null;
+ sock._websocket.onopen();
+ expect(sock._mode).to.equal('base64');
+ });
+
+ it('should call the open event handler on opening', function () {
+ sock._websocket.onopen();
+ expect(sock._eventHandlers.open).to.have.been.calledOnce;
+ });
+
+ it('should call the close event handler on closing', function () {
+ sock._websocket.onclose();
+ expect(sock._eventHandlers.close).to.have.been.calledOnce;
+ });
+
+ it('should call the error event handler on error', function () {
+ sock._websocket.onerror();
+ expect(sock._eventHandlers.error).to.have.been.calledOnce;
+ });
+ });
+
+ after(function () {
+ WebSocket = old_WS;
+ });
+ });
+
+ describe('WebSocket Receiving', function () {
+ var sock;
+ beforeEach(function () {
+ sock = new Websock();
+ });
+
+ it('should support decoding base64 string data to add it to the receive queue', function () {
+ var msg = { data: Base64.encode([1, 2, 3]) };
+ sock._mode = 'base64';
+ sock._recv_message(msg);
+ expect(sock.rQshiftStr(3)).to.equal('\x01\x02\x03');
+ });
+
+ it('should support adding binary Uint8Array data to the receive queue', function () {
+ var msg = { data: new Uint8Array([1, 2, 3]) };
+ sock._mode = 'binary';
+ sock._recv_message(msg);
+ expect(sock.rQshiftStr(3)).to.equal('\x01\x02\x03');
+ });
+
+ it('should call the message event handler if present', function () {
+ sock._eventHandlers.message = sinon.spy();
+ var msg = { data: Base64.encode([1, 2, 3]) };
+ sock._mode = 'base64';
+ sock._recv_message(msg);
+ expect(sock._eventHandlers.message).to.have.been.calledOnce;
+ });
+
+ it('should not call the message event handler if there is nothing in the receive queue', function () {
+ sock._eventHandlers.message = sinon.spy();
+ var msg = { data: Base64.encode([]) };
+ sock._mode = 'base64';
+ sock._recv_message(msg);
+ expect(sock._eventHandlers.message).not.to.have.been.called;
+ });
+
+ it('should compact the receive queue', function () {
+ // NB(sross): while this is an internal implementation detail, it's important to
+ // test, otherwise the receive queue could become very large very quickly
+ sock._rQ = [0, 1, 2, 3, 4, 5];
+ sock.set_rQi(6);
+ sock._rQmax = 3;
+ var msg = { data: Base64.encode([1, 2, 3]) };
+ sock._mode = 'base64';
+ sock._recv_message(msg);
+ expect(sock._rQ.length).to.equal(3);
+ expect(sock.get_rQi()).to.equal(0);
+ });
+
+ it('should call the error event handler on an exception', function () {
+ sock._eventHandlers.error = sinon.spy();
+ sock._eventHandlers.message = sinon.stub().throws();
+ var msg = { data: Base64.encode([1, 2, 3]) };
+ sock._mode = 'base64';
+ sock._recv_message(msg);
+ expect(sock._eventHandlers.error).to.have.been.calledOnce;
+ });
+ });
+
+ describe('Data encoding', function () {
+ before(function () { FakeWebSocket.replace(); });
+ after(function () { FakeWebSocket.restore(); });
+
+ describe('as binary data', function () {
+ var sock;
+ beforeEach(function () {
+ sock = new Websock();
+ sock.open('ws://', 'binary');
+ sock._websocket._open();
+ });
+
+ it('should convert the send queue into an ArrayBuffer', function () {
+ sock._sQ = [1, 2, 3];
+ var res = sock._encode_message(); // An ArrayBuffer
+ expect(new Uint8Array(res)).to.deep.equal(new Uint8Array(res));
+ });
+
+ it('should properly pass the encoded data off to the actual WebSocket', function () {
+ sock.send([1, 2, 3]);
+ expect(sock._websocket._get_sent_data()).to.deep.equal([1, 2, 3]);
+ });
+ });
+
+ describe('as Base64 data', function () {
+ var sock;
+ beforeEach(function () {
+ sock = new Websock();
+ sock.open('ws://', 'base64');
+ sock._websocket._open();
+ });
+
+ it('should convert the send queue into a Base64-encoded string', function () {
+ sock._sQ = [1, 2, 3];
+ expect(sock._encode_message()).to.equal(Base64.encode([1, 2, 3]));
+ });
+
+ it('should properly pass the encoded data off to the actual WebSocket', function () {
+ sock.send([1, 2, 3]);
+ expect(sock._websocket._get_sent_data()).to.deep.equal([1, 2, 3]);
+ });
+
+ });
+
+ });
+});