Browse Source

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
Solly Ross 11 years ago
parent
commit
2cccf7530c
3 changed files with 877 additions and 349 deletions
  1. 301 349
      include/websock.js
  2. 96 0
      tests/fake.websocket.js
  3. 480 0
      tests/test.websock.js

+ 301 - 349
include/websock.js

@@ -14,7 +14,7 @@
  * read binary data off of the receive queue.
  * read binary data off of the receive queue.
  */
  */
 
 
-/*jslint browser: true, bitwise: false, plusplus: false */
+/*jslint browser: true, bitwise: true */
 /*global Util, Base64 */
 /*global Util, Base64 */
 
 
 
 
@@ -43,382 +43,334 @@ if (window.WebSocket && !window.WEB_SOCKET_FORCE_FLASH) {
         }
         }
         Util.load_scripts(["web-socket-js/swfobject.js",
         Util.load_scripts(["web-socket-js/swfobject.js",
                            "web-socket-js/web_socket.js"]);
                            "web-socket-js/web_socket.js"]);
-    }());
+    })();
 }
 }
 
 
 
 
 function Websock() {
 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 {
                 } 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 {
             } 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();
-
-}
+    };
+})();

+ 96 - 0
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;
+        }
+    };
+})();

+ 480 - 0
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]);
+            });
+
+        });
+
+    });
+});