diff --git a/lib/app/js/services/Socket.js b/lib/app/js/services/Socket.js index 50985c52..f39a9195 100644 --- a/lib/app/js/services/Socket.js +++ b/lib/app/js/services/Socket.js @@ -5,13 +5,22 @@ angular.module('sgApp') var socket, connected = false, + port = '', + deferredEventListeners = [], service = { - + setPort: function(serverPort) { + port = serverPort; + }, isAvailable: function() { - return (typeof window.io !== 'undefined'); + return (typeof $window.io !== 'undefined'); }, on: function(eventName, listener) { - if (socket) { + deferredEventListeners.push({ + event: eventName, + listener: listener + }); + + if (this.isConnected()) { socket.on(eventName, function() { var args = arguments; $rootScope.$apply(function() { @@ -20,7 +29,6 @@ angular.module('sgApp') }); } }, - emit: function(eventName, data, callback) { if (socket) { socket.emit(eventName, data, function() { @@ -33,30 +41,41 @@ angular.module('sgApp') }); } }, - isConnected: function() { - return connected; - } + return socket !== undefined && connected === true; + }, + connect: connect }; - if (service.isAvailable()) { - socket = $window.io.connect('/'); - - service.on('connect', function() { - connected = true; - $rootScope.$broadcast('socket connected'); - }); + function connect() { + if (service.isAvailable()) { + var url = port ? ':' + port + '/' : '/'; + if (connected) { + socket.disconnect(); + } - service.on('disconnect', function() { - connected = false; - $rootScope.$broadcast('socket disconnected'); - }); + socket = $window.io.connect(url); - service.on('error', function(err) { - $rootScope.$broadcast('socket error', err); - }); + deferredEventListeners.forEach(function(deferred) { + socket.on(deferred.event, deferred.listener); + }); + } } + service.on('connect', function() { + connected = true; + $rootScope.$broadcast('socket connected'); + }); + + service.on('disconnect', function() { + connected = false; + $rootScope.$broadcast('socket disconnected'); + }); + + service.on('error', function(err) { + $rootScope.$broadcast('socket error', err); + }); + return service; }); diff --git a/lib/app/js/services/Styleguide.js b/lib/app/js/services/Styleguide.js index 86413a59..7f8fb3fd 100644 --- a/lib/app/js/services/Styleguide.js +++ b/lib/app/js/services/Styleguide.js @@ -28,6 +28,11 @@ angular.module('sgApp') _this.config.data = response.config; _this.variables.data = response.variables; _this.sections.data = response.sections; + + if (!Socket.isConnected()) { + Socket.setPort(response.config.port); + Socket.connect(); + } }); }; diff --git a/lib/styleguide.js b/lib/styleguide.js index 446e3ff9..945c5bbe 100644 --- a/lib/styleguide.js +++ b/lib/styleguide.js @@ -49,7 +49,7 @@ function sanitizeOptions(opt) { commonClass: opt.commonClass || '', styleVariables: opt.styleVariables || false, server: opt.server || false, - port: opt.port, + port: opt.port || 3000, rootPath: opt.rootPath, filesConfig: opt.filesConfig }; @@ -88,7 +88,7 @@ function generateInheritedWrappers(json) { } function copyUsedOptionsToJsonConfig(opt, json) { - var used = ['appRoot', 'extraHead', 'commonClass', 'title']; + var used = ['appRoot', 'extraHead', 'commonClass', 'title', 'port']; json.config = {}; used.forEach(function(prop) { json.config[prop] = _.cloneDeep(opt[prop]); @@ -362,7 +362,7 @@ module.exports.server = function(options) { }; function startServer(options) { - var port = options.port || 3000; + var port = options.port; // Ignore start server if we alrady have instance running if (!serverInstance) { serverInstance = sgServer(options); diff --git a/test/angular/unit/services/Socket.test.js b/test/angular/unit/services/Socket.test.js index 03050474..30e28f6d 100644 --- a/test/angular/unit/services/Socket.test.js +++ b/test/angular/unit/services/Socket.test.js @@ -5,10 +5,15 @@ describe('Service: Socket', function() { var service, rootScope, fakeIo, - fakeSocket; + fakeSocket, + fakeWindow = {}; beforeEach(angular.mock.module('sgApp')); + beforeEach(module(function($provide) { + $provide.value('$window', fakeWindow); + })); + beforeEach(function() { fakeSocket = { listeners: {}, @@ -25,10 +30,11 @@ describe('Service: Socket', function() { if (cb) { cb.call(undefined); } - }) + }), + disconnect: sinon.spy() }; - window.io = fakeIo = { + fakeWindow.io = fakeIo = { connect: sinon.stub().returns(fakeSocket) }; @@ -38,8 +44,32 @@ describe('Service: Socket', function() { }); }); - it('connects to path "/" using window.io', function() { - expect(fakeIo.connect).to.have.been.calledWith('/'); + describe('.connect()', function() { + + it('does not try to connect if socket.io is not available', function() { + delete fakeWindow.io; + service.connect(); + expect(fakeIo.connect).not.to.have.been.called; + }); + + it('connects to path "/" using window.io when no port is set', function() { + service.connect(); + expect(fakeIo.connect).to.have.been.calledWith('/'); + }); + + it('connects to path ":/" using window.io when port is set', function() { + service.setPort(3298); + service.connect(); + expect(fakeIo.connect).to.have.been.calledWith(':3298/'); + }); + + it('calls socket.disconnect() if already connected', function() { + connect(); + expect(service.isConnected()).to.eql(true); + service.connect(); + expect(fakeSocket.disconnect).to.have.been.called; + }); + }); describe('socket event listener', function() { @@ -47,6 +77,7 @@ describe('Service: Socket', function() { var broadcast; beforeEach(function() { broadcast = sinon.spy(rootScope, '$broadcast'); + connect(); }); it('for "connect" is broadcast via $rootScope as "socket connected" event', function() { @@ -74,17 +105,39 @@ describe('Service: Socket', function() { beforeEach(function() { listener = sinon.spy(); apply = sinon.spy(rootScope, '$apply'); - service.on('onTest', listener); }); - it('registers socket event listener with the same name', function() { - expect(fakeSocket.listeners.onTest.length).to.eql(1); + describe('when socket connection is open', function() { + + beforeEach(function() { + connect(); + service.on('onTest', listener); + }); + + it('registers socket event listener with the same name', function() { + expect(fakeSocket.listeners.onTest.length).to.eql(1); + }); + + it('applies the listener function through $rootScope.$apply', function() { + fakeSocket.emit('onTest'); + expect(apply).to.have.been.called; + expect(listener).to.have.been.called; + }); + }); - it('applies the listener function through $rootScope.$apply', function() { - fakeSocket.emit('onTest'); - expect(apply).to.have.been.called; - expect(listener).to.have.been.called; + describe('when socket connection is not open', function() { + + beforeEach(function() { + service.on('onTest', listener); + }); + + it('registers socket event listener with the same name when socket connects', function() { + expect(fakeSocket.listeners.onTest).to.be.undefined; + connect(); + expect(fakeSocket.listeners.onTest.length).to.eql(1); + }); + }); }); @@ -98,20 +151,32 @@ describe('Service: Socket', function() { apply = sinon.spy(rootScope, '$apply'); }); - it('emits a socket event with the same name', function() { + it('does nothing when socket is not available', function() { service.emit('emitTest'); - expect(fakeSocket.emit).to.have.been.calledWith('emitTest'); + expect(fakeSocket.emit).not.to.have.been.called; + expect(apply).not.to.have.been.called; }); - it('passes the given data object to socket.emit', function() { - service.emit('emitTest', data); - expect(fakeSocket.emit).to.have.been.calledWith('emitTest', data); - }); + describe('when socket is available', function() { + + beforeEach(connect); + + it('emits a socket event with the same name', function() { + service.emit('emitTest'); + expect(fakeSocket.emit).to.have.been.calledWith('emitTest'); + }); + + it('passes the given data object to socket.emit', function() { + service.emit('emitTest', data); + expect(fakeSocket.emit).to.have.been.calledWith('emitTest', data); + }); + + it('applies the callback through $rootScope.$apply', function() { + service.emit('emitTest', data, callback); + expect(apply).to.have.been.called; + expect(callback).to.have.been.called; + }); - it('applies the callback through $rootScope.$apply', function() { - service.emit('emitTest', data, callback); - expect(apply).to.have.been.called; - expect(callback).to.have.been.called; }); }); @@ -123,12 +188,13 @@ describe('Service: Socket', function() { }); it('returns true after socket has emitted "connect" event', function() { + service.connect(); fakeSocket.emit('connect'); expect(service.isConnected()).to.eql(true); }); it('returns false after socket has emitted "disconnect" event', function() { - fakeSocket.emit('connect'); + connect(); expect(service.isConnected()).to.eql(true); fakeSocket.emit('disconnect'); expect(service.isConnected()).to.eql(false); @@ -139,15 +205,20 @@ describe('Service: Socket', function() { describe('.isAvailable()', function() { it('returns true if window.io is not undefined', function() { - window.io = sinon.stub(); + fakeWindow.io = sinon.stub(); expect(service.isAvailable()).to.eql(true); }); it('returns false if window.io is undefined', function() { - window.io = undefined; + delete fakeWindow.io; expect(service.isAvailable()).to.eql(false); }); }); + function connect() { + service.connect(); + fakeSocket.emit('connect'); + } + }); diff --git a/test/angular/unit/services/Styleguide.test.js b/test/angular/unit/services/Styleguide.test.js new file mode 100644 index 00000000..2db6e9c8 --- /dev/null +++ b/test/angular/unit/services/Styleguide.test.js @@ -0,0 +1,259 @@ +describe('Service: Styleguide', function() { + + 'use strict'; + + var service, + http, + rootScope, + socketService, + debounce = sinon.stub().returnsArg(1), + json = { + config: { + foo: 'bar', + port: 123 + }, + variables: ['a', 'b'], + sections: ['1', '2'] + }; + + beforeEach(angular.mock.module('sgApp')); + + beforeEach(module(function($provide) { + socketService = { + eventHandlers: {}, + setPort: sinon.spy(), + isAvailable: sinon.stub(), + emit: sinon.spy(), + isConnected: sinon.stub(), + connect: sinon.spy(), + on: sinon.spy(function(event, handler) { + socketService.eventHandlers[event] = socketService.eventHandlers[event] || []; + socketService.eventHandlers[event].push(handler); + }) + }; + $provide.value('Socket', socketService); + $provide.value('debounce', debounce); + })); + + beforeEach(function() { + inject(function($httpBackend, $rootScope, _Styleguide_) { + http = $httpBackend; + rootScope = $rootScope; + service = _Styleguide_; + }); + + http.whenGET('styleguide.json').respond(json); + }); + + it('GETs styleguide.json on initialization', function() { + http.expectGET('styleguide.json'); + http.flush(); + + http.verifyNoOutstandingExpectation(); + http.verifyNoOutstandingRequest(); + }); + + it('GETs styleguide.json $rootScope broadcast event "styles changed"', function() { + http.flush(); + http.resetExpectations(); + http.expectGET('styleguide.json'); + rootScope.$broadcast('styles changed'); + http.flush(); + + http.verifyNoOutstandingExpectation(); + http.verifyNoOutstandingRequest(); + }); + + it('GETs styleguide.json $rootScope broadcast event "progress end"', function() { + http.flush(); + http.resetExpectations(); + http.expectGET('styleguide.json'); + rootScope.$broadcast('progress end'); + http.flush(); + + http.verifyNoOutstandingExpectation(); + http.verifyNoOutstandingRequest(); + }); + + it('debounces #get by 800ms', function() { + expect(debounce).to.have.been.calledWith(800, sinon.match.func); + }); + + describe('.get()', function() { + + beforeEach(function() { + http.expectGET('styleguide.json'); + service.get(); + http.flush(); + }); + + it('GETs styleguide.json', function() { + http.verifyNoOutstandingExpectation(); + http.verifyNoOutstandingRequest(); + }); + + it('stores response json config as config.data', function() { + expect(service.config.data).to.deep.equal(json.config); + }); + + it('stores response json variables as variables.data', function() { + expect(service.variables.data).to.deep.equal(json.variables); + }); + + it('stores response json sections as sections.data', function() { + expect(service.sections.data).to.deep.equal(json.sections); + }); + + describe('after completion', function() { + + beforeEach(function() { + socketService.connect.reset(); + socketService.isConnected.reset(); + }); + + it('does not connect Socket if already connected', function() { + socketService.isConnected.returns(true); + service.get(); + http.flush(); + + expect(socketService.isConnected).to.have.been.calledOnce; + expect(socketService.connect).not.to.have.been.calledOnce; + }); + + describe('if Socket is not connected', function() { + + beforeEach(function() { + socketService.isConnected.returns(false); + service.get(); + http.flush(); + }); + + it('sets Socket port to what was received in styleguide.json', function() { + expect(socketService.isConnected).to.have.been.calledOnce; + expect(socketService.setPort).to.have.been.calledWithExactly(json.config.port); + }); + + it('connects Socket', function() { + expect(socketService.isConnected).to.have.been.calledOnce; + expect(socketService.connect).to.have.been.calledOnce; + }); + + }); + + }); + + }); + + describe('socket event listener', function() { + + describe('for "styleguide compile error"', function() { + + var error = { name: 'compile error', message: 'compile failed' }; + + beforeEach(function() { + service.status.hasError = false; + service.status.error = { old: 'value' }; + service.status.errType = 'shouldChange'; + socketService.eventHandlers['styleguide compile error'][0].call(undefined, error); + }); + + it('is registered', function() { + expect(socketService.on).to.have.been.calledWith('styleguide compile error', sinon.match.func); + }); + + it('sets Styleguide.status.hasError to true', function() { + expect(service.status.hasError).to.eql(true); + }); + + it('sets Styleguide.status.error to error object', function() { + expect(service.status.error).to.deep.eql(error); + }); + + it('sets Styleguide.status.errType to compile', function() { + expect(service.status.errType).to.eql('compile'); + }); + + }); + + describe('for "styleguide validation error"', function() { + + var error = { name: 'validation error', message: 'validation failed' }; + + beforeEach(function() { + service.status.hasError = false; + service.status.error = { old: 'value' }; + service.status.errType = 'shouldChange'; + socketService.eventHandlers['styleguide validation error'][0].call(undefined, error); + }); + + it('is registered', function() { + expect(socketService.on).to.have.been.calledWith('styleguide validation error', sinon.match.func); + }); + + it('sets Styleguide.status.hasError to true', function() { + expect(service.status.hasError).to.eql(true); + }); + + it('sets Styleguide.status.error to error object', function() { + expect(service.status.error).to.deep.eql(error); + }); + + it('sets Styleguide.status.errType to validation', function() { + expect(service.status.errType).to.eql('validation'); + }); + + }); + + describe('for "styleguide compile success"', function() { + + var error = { name: 'old error', message: 'old message' }, + errType = 'old type'; + + beforeEach(function() { + service.status.hasError = true; + service.status.error = error; + service.status.errType = errType; + socketService.eventHandlers['styleguide compile success'][0].call(undefined); + }); + + it('is registered', function() { + expect(socketService.on).to.have.been.calledWith('styleguide compile success', sinon.match.func); + }); + + it('sets Styleguide.status.hasError to false', function() { + expect(service.status.hasError).to.eql(false); + }); + + it('leaves Styleguide.status.error untouched', function() { + expect(service.status.error).to.deep.eql(error); + }); + + it('leaves Styleguide.status.errType untouched', function() { + expect(service.status.errType).to.eql(errType); + }); + + }); + + }); + + describe('status', function() { + + describe('initially', function() { + + it('does not have error', function() { + expect(service.status.hasError).to.eql(false); + }); + + it('error is empty object', function() { + expect(service.status.error).to.eql({}); + }); + + it('errType is empty string', function() { + expect(service.status.errType).to.eql(''); + }); + + }); + + }); + +}); diff --git a/test/integration/structure.test.js b/test/integration/structure.test.js index 96953085..382c1b9d 100644 --- a/test/integration/structure.test.js +++ b/test/integration/structure.test.js @@ -205,6 +205,10 @@ function sharedStyleguideJSON() { expect(this.jsonData.config.commonClass).to.eql(['custom-class-1', 'custom-class-2']); }); + it('should contain default port 3000 when no port is defined', function() { + expect(this.jsonData.config.port).to.eql(3000); + }); + it('should contain all style variable names from defined file', function() { expect(this.jsonData.variables[0].name).to.eql('color-red'); expect(this.jsonData.variables[1].name).to.eql('color-green');