diff options
author | Solly Ross <sross@redhat.com> | 2014-05-22 16:57:55 -0400 |
---|---|---|
committer | Solly Ross <sross@redhat.com> | 2014-09-15 16:46:02 -0400 |
commit | 2cccf7530c272de3627fa871da8e6ed44f437edb (patch) | |
tree | 8b4b9f1f4667413620f8eee44e441d85b26b7782 | |
parent | ee7d4c61c6c2b39bc257d9fc1c20d2b2a479d370 (diff) | |
download | novnc-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.js | 650 | ||||
-rw-r--r-- | tests/fake.websocket.js | 96 | ||||
-rw-r--r-- | tests/test.websock.js | 480 |
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]); + }); + + }); + + }); +}); |