UNPKG

@jknott/typescript-robust-websocket

Version:

A robust reconnecting WebSocket client for the browser with ts compatability based on robust-websocket

433 lines (374 loc) 14.4 kB
describe('RobustWebSocket', function() { var ws, serverUrl = location.origin.replace('http', 'ws'), isSafari = window.webkitCancelAnimationFrame && !window.webkitRTCPeerConnection, isIE = Object.hasOwnProperty.call(window, 'ActiveXObject'), isEdge = !!window.MSStream, isIEOrEdge = isIE || isEdge this.retries(3) afterEach(function() { Mocha.onLine = true try { if (ws) { ws.listeners.length = 0 ws.onclose = null ws.close() } } catch (e) {} }) function wrap(fn, done) { return function() { try { fn.apply(this, arguments) } catch(e) { done(e) } } } describe('web standards behavior', function() { it('should forward messages and errors to the client via event listeners', function(done) { ws = new RobustWebSocket(serverUrl + '/echo') ws.addEventListener('open', wrap(function(evt) { this.should.equal(ws) evt.target.should.be.instanceof(WebSocket) evt.reconnects.should.equal(0) evt.attempts.should.equal(1) ws.send('hello!') }, done)) var onmessage = sinon.spy(function(evt) { evt.data.should.equal('hello!') evt.target.should.be.instanceof(WebSocket) ws.close() }) ws.addEventListener('message', wrap(onmessage, done)) ws.addEventListener('close', wrap(function() { onmessage.should.have.been.calledOnce done() }, done)) }) it('should forward messages and errors to the client via on* properties', function(done) { ws = new RobustWebSocket(serverUrl + '/echo') ws.onopen = wrap(function(evt) { this.should.equal(ws) evt.target.should.be.instanceof(WebSocket) evt.reconnects.should.equal(0) evt.attempts.should.equal(1) ws.send('hello!') }, done) ws.onmessage = sinon.spy(wrap(function(evt) { evt.data.should.equal('hello!') evt.target.should.be.instanceof(WebSocket) ws.close() }, done)) ws.addEventListener('close', wrap(function(evt) { ws.onmessage.should.have.been.calledOnce evt.code.should.equal(1000) done() }, done)) }) it('should proxy read only properties', function() { ws = new RobustWebSocket(serverUrl) ws.url.should.contain(serverUrl) ws.protocol.should.equal('') ws.readyState.should.equal(WebSocket.CONNECTING) ws.bufferedAmount.should.equal(0) return pollUntilPassing(function() { ws.readyState.should.equal(WebSocket.OPEN) }) }) it('should rethrow errors', !isIEOrEdge && function() { (function() { new RobustWebSocket('localhost:11099') }).should.throw(Error) ;(function() { ws = new RobustWebSocket(serverUrl) ws.send() }).should.throw(Error) }) it('should work in a web worker', !isSafari && !isEdge && function(done) { var worker = new Worker('./webworker.js') worker.onmessage = function(event) { event.data.should.equal('howdy') done() } }) it('should work with different binary types') it('should support the protocols parameter') }) function shouldNotReconnect(code) { return function() { ws = new RobustWebSocket(serverUrl + '/?exitCode=' + code + '&exitMessage=alldone') ws.onclose = sinon.spy(function(evt) { if (!isIEOrEdge) { evt.code.should.equal(code) evt.reason.should.equal('alldone') } }) ws.onopen = sinon.spy() return pollUntilPassing(function() { ws.onclose.should.have.been.calledOnce ws.onopen.should.have.been.calledOnce ws.readyState.should.equal(WebSocket.CLOSED) }).then(function() { return Promise.delay(1000) }).then(function() { ws.onclose.should.have.been.calledOnce ws.onopen.should.have.been.calledOnce ws.readyState.should.equal(WebSocket.CLOSED) }) } } describe('robustness', function() { it('should reconnect when a server reboots (1012)', function() { ws = new RobustWebSocket(serverUrl + '/?exitCode=1012&exitMessage=alldone&delay=250') ws.onclose = sinon.spy(function(evt) { if (!isIEOrEdge) { evt.code.should.equal(1012) evt.reason.should.equal('alldone') } }) ws.onopen = sinon.spy() return pollUntilPassing(function() { ws.onopen.callCount.should.be.greaterThan(2) ws.onclose.callCount.should.be.greaterThan(1) }) }) it('should not reconnect on normal disconnects (1000)', !isIEOrEdge && shouldNotReconnect(1000)) it('should call shouldReconnect on normal disconnects if handle1000 is true', !isIEOrEdge && function() { // Issue #14 var attemptLog = [], rounds = 0, shouldReconnect = sinon.spy(function(event, ws) { event.type.should.equal('close') event.currentTarget.should.be.instanceof(WebSocket) // ws.attempts is reset on each successful open, so we separately track the number of open-close // cycles using `rounds` attemptLog.push(ws.attempts) return rounds++ < 2 && 100 }) shouldReconnect.handle1000 = true; ws = new RobustWebSocket(serverUrl + '/?exitCode=1000&exitMessage=alldone&delay=250', null, { shouldReconnect: shouldReconnect }) ws.onclose = sinon.spy(function(evt) { evt.code.should.equal(1000) }) return pollUntilPassing(function() { attemptLog.should.deep.equal([0, 0, 0]) ws.onclose.should.have.been.calledThrice shouldReconnect.should.have.been.calledThrice ws.readyState.should.equal(WebSocket.CLOSED) }) }) it('should not reconnect 1008 by default (HTTP 400 equvalent)', !isIEOrEdge && shouldNotReconnect(1008)) it('should not reconnect 1011 by default (HTTP 500 equvalent)', !isIEOrEdge && shouldNotReconnect(1011)) it('should emit connecting events when reconnecting (1001)', function() { ws = new RobustWebSocket(serverUrl + '/?exitCode=1001') ws.onclose = sinon.spy(function(evt) { !isIEOrEdge && evt.code.should.equal(1001) evt.reason.should.equal('') }) var reconnectingListener = sinon.spy() ws.addEventListener('connecting', reconnectingListener) return pollUntilPassing(function() { reconnectingListener.should.have.been.called var event = reconnectingListener.lastCall.args[0] event.type.should.equal('connecting') event.attempts.should.equal(1) }) }) // Safari never calls the onerror callback. The connection will just timeout in that case. it('should retry the initial connection if it failed', !isSafari && function() { var attemptLog = [], shouldReconnect = sinon.spy(function(event, ws) { event.type.should.equal('close') event.currentTarget.should.be.instanceof(WebSocket) // since ws.attempts refers to the current attempts on the websocket, we need to save them // rather than use sinon.firstCall.args[0].attempts attemptLog.push(ws.attempts) return ws.attempts < 3 && 500 }) ws = new RobustWebSocket('ws://localhost:88', null, { shouldReconnect: shouldReconnect }) ws.onclose = sinon.spy(function(evt) { evt.code.should.equal(1006) evt.reason.should.equal('') }) ws.onerror = sinon.spy(function(e) { e.type.should.equal('error') }) return pollUntilPassing(function() { ws.onerror.should.have.been.calledThrice ws.onclose.should.have.been.calledThrice shouldReconnect.should.have.been.calledThrice ws.readyState.should.equal(WebSocket.CLOSED) attemptLog.should.deep.equal([1, 2, 3]) }).then(function() { return Promise.delay(1500) }).then(function() { ws.onclose.should.have.been.calledThrice ws.readyState.should.equal(WebSocket.CLOSED) }) }) it('should not try to reconnect while offline, trying again when online', function() { this.timeout(8000) Mocha.onLine = false var shouldReconnect = sinon.spy(function() { return 0 }) ws = new RobustWebSocket(serverUrl + '/?exitCode=1002&delay=500', null, shouldReconnect) ws.onopen = sinon.spy() return pollUntilPassing(function() { ws.onopen.should.have.been.calledOnce shouldReconnect.should.have.not.been.called }).then(function() { return Promise.delay(1000) }).then(function() { ws.onopen.should.have.been.calledOnce shouldReconnect.should.have.not.been.called Mocha.onLine = true window.dispatchEvent(new CustomEvent('online')) return pollUntilPassing(function() { shouldReconnect.should.have.been.calledOnce ws.onopen.should.have.been.calledTwice }) }) }) it('should immediately close the websocket when going offline rather than waiting for a timeout', function() { this.timeout(8000) var shouldReconnect = sinon.spy(function() { return 0 }) ws = new RobustWebSocket(serverUrl + '/echo', null, shouldReconnect) ws.onopen = sinon.spy() ws.onclose = sinon.spy() return pollUntilPassing(function() { ws.onopen.should.have.been.calledOnce shouldReconnect.should.have.not.been.called }).then(function() { return Promise.delay(100) }).then(function() { window.dispatchEvent(new CustomEvent('offline')) return pollUntilPassing(function() { ws.onclose.should.have.been.calledOnce ws.readyState.should.equal(WebSocket.CLOSED) }) }).then(function() { return Promise.delay(1000) }).then(function() { ws.onclose.should.have.been.calledOnce ws.readyState.should.equal(WebSocket.CLOSED) window.dispatchEvent(new CustomEvent('online')) return pollUntilPassing(function() { ws.onopen.should.have.been.calledTwice shouldReconnect.should.have.been.calledOnce }) }).then(function() { return Promise.delay(1000) }).then(function() { ws.onopen.should.have.been.calledTwice shouldReconnect.should.have.been.calledOnce ws.readyState.should.equal(WebSocket.OPEN) }) }) it('should not reconnect a websocket that was explicitly closed when going back online', function() { ws = new RobustWebSocket(serverUrl + '/echo', null, function() { return 0 }) ws.onopen = sinon.spy() ws.onclose = sinon.spy() return pollUntilPassing(function() { ws.readyState.should.equal(WebSocket.OPEN) }).then(function() { Mocha.onLine = false ws.close() return pollUntilPassing(function() { ws.readyState.should.equal(WebSocket.CLOSED) ws.onclose.should.have.been.calledOnce }) }).then(function() { return Promise.delay(300) }).then(function() { window.dispatchEvent(new CustomEvent('online')) return Promise.delay(500) }).then(function() { ws.onclose.should.have.been.calledOnce ws.onclose.should.have.been.calledOnce }) }) }) describe('extra features', function() { it('should emit a timeout event if the connection timed out') it('should allow the socket to be reopened', function() { ws = new RobustWebSocket(serverUrl + '/echo') ws.onclose = sinon.spy() ws.onopen = sinon.spy() return pollUntilPassing(function() { ws.onopen.should.have.been.calledOnce ws.onclose.should.have.not.been.called ws.readyState.should.equal(WebSocket.OPEN) }).then(function() { ws.close() return pollUntilPassing(function() { ws.onopen.should.have.been.calledOnce ws.onclose.should.have.been.calledOnce ws.readyState.should.equal(WebSocket.CLOSED) }) }).then(function() { return Promise.delay(100) }).then(function() { ws.open() return pollUntilPassing(function() { ws.onopen.should.have.been.calledTwice ws.onclose.should.have.been.calledOnce ws.readyState.should.equal(WebSocket.OPEN) }) }) }) it('should not reconnect if the socket is already opened when open is called', function() { ws = new RobustWebSocket(serverUrl + '/echo') ws.onclose = sinon.spy() ws.onopen = sinon.spy() return pollUntilPassing(function() { ws.onopen.should.have.been.calledOnce ws.readyState.should.equal(WebSocket.OPEN) }).then(function() { return Promise.delay(100) }).then(function() { ws.open() return Promise.delay(300) }).then(function() { ws.onopen.should.have.been.calledOnce ws.onclose.should.have.not.been.called ws.readyState.should.equal(WebSocket.OPEN) }) }) it('should not automatically open the connection if requested', function() { ws = new RobustWebSocket(serverUrl + '/echo', null, { automaticOpen: false }) ws.onclose = sinon.spy() ws.onopen = sinon.spy() return Promise.delay(400).then(function() { ws.onopen.should.have.not.been.called ws.onclose.should.have.not.been.called should.not.exist(ws.readyState) ws.open() return pollUntilPassing(function() { ws.onopen.should.have.been.calledOnce ws.readyState.should.equal(WebSocket.OPEN) }) }) }) it('should not close a socket if ignoreConnectivityEvents is in use', function() { ws = new RobustWebSocket(serverUrl + '/echo', null, { ignoreConnectivityEvents: true, shouldReconnect: function() { return 0 } }) ws.onclose = sinon.spy() return pollUntilPassing(function() { ws.readyState.should.equal(WebSocket.OPEN) }).then(function() { Mocha.onLine = false return Promise.delay(300) }).then(function() { ws.readyState.should.equal(WebSocket.OPEN) ws.onclose.should.not.have.been.called }) }) }) })