diff --git a/.gitignore b/.gitignore index 5431a602..c74d23f0 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ src/components/*/* !src/components/angular-touch/angular*.js !src/components/angular-touch/angular*.map !src/components/angular-bootstrap/*.js +!src/components/angular-storage/*.js !src/components/bootstrap-css/css !src/components/bootstrap-css/img diff --git a/.travis.yml b/.travis.yml index 29f80094..7cf38a3b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,17 @@ language: node_js +dist: trusty +sudo: true node_js: - - 0.10 + - "stable" before_install: - "npm i -g bower karma-cli" before_script: + - export CHROME_BIN=/usr/bin/google-chrome - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start + - sudo apt-get install -y libappindicator1 fonts-liberation + - wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb + - sudo dpkg -i google-chrome*.deb after_script: - ls ./coverage - 'npm install coveralls@2.10.0 && cat "./coverage/Firefox 31.0.0 (Linux)/lcov.info" | coveralls' diff --git a/bower.json b/bower.json index d67cc0ee..1c2f101e 100644 --- a/bower.json +++ b/bower.json @@ -17,6 +17,7 @@ "dependencies": { "angular": "~1.2.20", "angular-bootstrap": "~0.11.0", + "angular-local-storage": "~0.6.0", "angular-mocks": "~1.2.20", "angular-sanitize": "~1.3.3", "angular-touch": "~1.3.4", diff --git a/karma.conf.js b/karma.conf.js index f6d4a0bc..cb9b70be 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -17,6 +17,7 @@ module.exports = function(config) { 'src/components/jquery/jquery.min.js', 'src/components/angular/angular.min.js', 'src/components/angular-mocks/angular-mocks.js', + 'src/components/angular-storage/angular-storage.js', 'src/components/q/q.js', 'src/components/idbwrapper/idbstore.js', 'spec/helpers/*.js', diff --git a/localserver.js b/localserver.js index add3d0c2..153152fb 100644 --- a/localserver.js +++ b/localserver.js @@ -37,5 +37,5 @@ routers.forEach(function(router) { app.listen(args.port, function() { console.log('Listening on port ', args.port); - console.log('open browser to http://localhost:{0}/'.format(args.port)); + console.log(`open browser to http://localhost:${args.port}/`); }); diff --git a/package.json b/package.json index f9fac7a7..a39220d9 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "start": "node localserver", - "test": "./node_modules/.bin/karma start --single-run --browsers Firefox --reporters dots,coverage" + "test": "./node_modules/.bin/karma start --single-run --reporters dots,coverage" }, "repository": { "type": "git", @@ -30,6 +30,7 @@ }, "dependencies": { "basic-auth-connect": "~1.0.0", + "lockfile": "~1.0.3", "uuid": "^3.1.0", "express": "^4.10.4", "express-session": "^1.5.0", diff --git a/server_modules/challenges.js b/server_modules/challenges.js index f0493eab..069ab11c 100644 --- a/server_modules/challenges.js +++ b/server_modules/challenges.js @@ -5,7 +5,7 @@ exports.route = function(app) { //get challenges over xhr, for hosted service app.get('/challenge/:year', function(req, res) { - var file = fileSystem.resolve('challenges/js/{0}.js'.format(req.params.year)); + var file = fileSystem.resolve(`challenges/js/${req.params.year}.js`); res.header('Content-Type','text/plain'); res.sendFile(file); diff --git a/server_modules/file_system.js b/server_modules/file_system.js index a704a6f2..27e961ab 100644 --- a/server_modules/file_system.js +++ b/server_modules/file_system.js @@ -33,7 +33,6 @@ exports.readFile = function(file) { if (exists) { resolve(exists); } else { - log.error("file not found {0}".format(file)); reject({ status: 404, message: 'file not found' @@ -42,7 +41,6 @@ exports.readFile = function(file) { }); }).then(function() { return Q.nfcall(fs.readFile, file, "utf-8").catch(function(e) { - log.error("error reading file {0}".format(file)); throw new Error({ status: 500, message: 'error reading file' @@ -77,15 +75,13 @@ exports.route = function(app) { var file = exports.getDataFilePath(req.params[0]); fs.stat(file, function(err, stat) { if (err) { - log.error("file not found {0}".format(file)); - res.status(404).send('file not found'); + utils.sendError(res, { status: 404, message: `file not found ${file}` }) return; } if (stat.isFile()) { fs.readFile(file, function(err, data) { if (err) { - log.error("error reading file {0}".format(file)); - res.status(500).send('error reading file'); + utils.sendError(res, { status: 500, message: `error reading file ${file}` }) return; } res.send(data); @@ -93,8 +89,7 @@ exports.route = function(app) { } else if (stat.isDirectory()) { fs.readdir(file, function(err, filenames) { if (err) { - log.error("error reading dir {0}".format(file)); - res.status(500).send('error reading dir'); + utils.sendError(res, { status: 500, message: `error reading dir ${file}` }) return; } // FIXME: this doesn't work for filenames containing @@ -103,15 +98,13 @@ exports.route = function(app) { return name.indexOf("\n") >= 0; }); if (hasNewline) { - log.error("invalid filename(s) {0}".format(filenames.join(', '))); - res.status(500).send('invalid filename(s)'); + utils.sendError(res, { status: 500, message: `invalid filename(s) ${filenames.join(', ')}` }) return; } res.send(filenames.join('\n')); }); } else { - log.error("error reading file {0}".format(file)); - res.status(500).send('error reading file'); + utils.sendError(res, { status: 500, message: `error reading file ${file}` }) return; } }); @@ -123,8 +116,7 @@ exports.route = function(app) { exports.writeFile(file, req.body).then(function() { res.status(200).end(); }).catch(function(err) { - log.error("error writing file {0}".format(err)); - res.status(500).send('error writing file'); + utils.sendError(res, { status: 500, message: `error writing file ${file}` }) }); }); @@ -133,8 +125,7 @@ exports.route = function(app) { var file = exports.getDataFilePath(req.params[0]); fs.unlink(file, function(err) { if (err) { - log.error("error removing file {0}".format(err)); - res.status(500).send('error removing file'); + utils.sendError(res, { status: 500, message: `error removing file ${file}` }) } res.status(200).end(); }); diff --git a/server_modules/lock.js b/server_modules/lock.js new file mode 100644 index 00000000..b98db512 --- /dev/null +++ b/server_modules/lock.js @@ -0,0 +1,33 @@ +//This module wraps lockfile with promises. +var lockfile = require('lockfile'); + +module.exports = function(filename, options) { + this.filename = filename; + this.options = options || {}; + + this.lock = function() { + var self = this; + + return new Promise(function(resolve, reject) { + lockfile.lock('scores.json.lock', self.options, function(err) { + if(err !err.startsWith('EEXIST')) { + reject(err); + } + + resolve(); + }); + }); + }; + + this.unlock = function() { + return new Promise(function(resolve, reject) { + lockfile.unlock('scores.json.lock', function(err) { + if(err && !err.startsWith('EEXIST')){ + reject(err); + } + + resolve(); + }); + }); + }; +} diff --git a/server_modules/log.js b/server_modules/log.js index 20113e55..8afc05a7 100644 --- a/server_modules/log.js +++ b/server_modules/log.js @@ -12,7 +12,7 @@ exports.middleware = function(req, res, next) { req.log = exports.log; - req.log.debug('Starting {0} {1}'.format(req.method, req.originalUrl)); + req.log.debug(`Starting ${req.method} ${req.originalUrl}`); next(); diff --git a/server_modules/scores.js b/server_modules/scores.js index ba65806b..711f715d 100644 --- a/server_modules/scores.js +++ b/server_modules/scores.js @@ -1,6 +1,9 @@ +var Lock = require('./lock'); var utils = require('./utils'); var fileSystem = require('./file_system'); +var log = require('./log').log; var Q = require('q'); +var id = require('uuid/v4'); function filterPublished(score) { return score.published; @@ -15,6 +18,45 @@ function reduceToMap(key) { } } +/** + * Atomically change scores file. + * Action callback is called with the current contents of the scores file, and is expected + * to return the new contents (or a Promise for it). + * A lock is acquired and held during the entire operation. + * @param action Callback that receives current scores.json object, must return new contents (or Promise for it) + * @return Promise for updated scores object + */ +function changeScores(action) { + var path = fileSystem.getDataFilePath('scores.json'); + var lock = new Lock('scores.json.lock', { retries: 5, retryWait: 100 }); + + console.log(lock.options); + + return lock.lock() + .then(() => fileSystem.readJsonFile(path)) + .catch((err) => { //Ignoring all file not found errors, and just returning empty scores.json + console.log("catching"); + if(err.message === 'file not found') { + console.log("hells yeah!"); + return { version:3, scores: [] }; + } else { + console.log("ho no! " + err.message); + throw err; + } + }) + .then(action) + .then(scores => { + return fileSystem.writeFile(path, JSON.stringify(scores)) + .then(() => { + return lock.unlock(); + }).catch((err) => { + return lock.unlock(); + }).then(function() { + return scores; + }); + }); +} + exports.route = function(app) { //get all, grouped by round @@ -32,7 +74,7 @@ exports.route = function(app) { return rounds; },{}); res.json(published); - }).catch(utils.sendError(res)).done(); + }).catch(err => utils.sendError(res, err)).done(); }); //get scores by round @@ -44,7 +86,84 @@ exports.route = function(app) { return score.published && score.round === round; }); res.json(scoresForRound); - }).catch(utils.sendError(res)).done(); + }).catch(err => utils.sendError(res, err)).done(); + }); + + //save a new score + app.post('/scores/create',function(req,res) { + var body = JSON.parse(req.body); + var scoresheet = body.scoresheet; + var score = body.score; + + fileSystem.writeFile(fileSystem.getDataFilePath("scoresheets/" + score.file), req.body) + .then(changeScores(function(result) { + if(typeof(score.id) === 'undefined') { + score.id = id(); + } + result.scores.push(score); + return result; + })) + .then(function(scores) { + res.json(scores).end(); + }).catch(err => utils.sendError(res, err)).done(); + + }); + + //delete a score at an id + app.post('/scores/delete/:id',function(req,res) { + changeScores(function(result) { + var index = result.scores.findIndex((score) => score.id === req.params.id); + if(index === -1) { + throw new Error(`Could not find score with id ${req.params.id}`); + } + result.scores.splice(index, 1); + return result; + }).then(function(scores) { + res.json(scores).end(); + }).catch(err => utils.sendError(res, err)).done(); }); + //edit a score at an id + app.post('/scores/update/:id',function(req,res) { + var score = JSON.parse(req.body); + changeScores(function(result) { + var index = result.scores.findIndex((score) => score.id === req.params.id); + if(index === -1) { + throw new Error(`Could not find score with id ${req.params.id}`); + } + result.scores[index] = score; + return result; + }).then(function(scores) { + res.json(scores).end(); + }).catch(err => utils.sendError(res, err)).done(); + }); + + }; + + +// For backward compatibility + +changeScores(function(scores) { + if(typeof(scores.version) === 'undefined') { + log.warn('Deprecated scores version. Updating to version 3.') + scores.forEach(score => score.id = id()) + return { + version: 3, + scores: scores + } + + } else if(scores.version === 3) { + return scores; + + } else if(scores.version === 2) { + log.warn('Deprecated scores version. Updating to version 3.'); + scores.scores.forEach(score => score.id = id()) + scores.version = 3; + return scores; + + } else { + throw new Error('Unkown scores version'); + } + +}); diff --git a/server_modules/teams.js b/server_modules/teams.js index e073adfc..aaee29b9 100644 --- a/server_modules/teams.js +++ b/server_modules/teams.js @@ -7,7 +7,7 @@ exports.route = function(app) { app.get('/teams',function(req,res) { fileSystem.readJsonFile(fileSystem.getDataFilePath('teams.json')).then(function(result) { res.json(result); - }).catch(utils.sendError(res)).done(); + }).catch(err => utils.sendError(res, err)).done(); }); app.get('/teams/:nr',function(req,res) { @@ -16,7 +16,7 @@ exports.route = function(app) { return team.number == req.params.nr; })[0]; res.json(team); - }).catch(utils.sendError(res)).done(); + }).catch(err => utils.sendError(res, err)).done(); }); }; diff --git a/server_modules/utils.js b/server_modules/utils.js index 670d753a..fb5f2a45 100644 --- a/server_modules/utils.js +++ b/server_modules/utils.js @@ -1,16 +1,11 @@ -exports.root = __dirname + '/../'; - -exports.sendError = function(res) { - return function(err) { - res.status(err.status).send(err.message); - } -} - -if (!String.prototype.format) { - String.prototype.format = function() { - var args = arguments; - return this.replace(/{(\d+)}/g, function(match, number) { - return typeof args[number] !== 'undefined' ? args[number] : match; - }); - }; -} +var log = require('./log').log; + +exports.root = __dirname + '/../'; + +exports.sendError = function(res, err) { + var status = err.status || 500; + var message = err.message || "Internal server error" + + log.error(message); + res.status(status).send(message); +} diff --git a/spec/controllers/ExportRankingDialiogControllerSpec.js b/spec/controllers/ExportRankingDialiogControllerSpec.js index 8a146949..6b77de64 100644 --- a/spec/controllers/ExportRankingDialiogControllerSpec.js +++ b/spec/controllers/ExportRankingDialiogControllerSpec.js @@ -14,7 +14,7 @@ describe('ExportRankingDialogController',function() { angular.mock.inject(function($controller,$rootScope,$q,_$timeout_) { $scope = $rootScope.$new(); $timeout = _$timeout_; - scoresMock = createScoresMock($q,fakeScoreboard); + scoresMock = createScoresMock(fakeScoreboard); handshakeMock = createHandshakeMock($q); stagesMock = createStagesMock(); controller = $controller('ExportRankingDialogController', { @@ -55,24 +55,6 @@ describe('ExportRankingDialogController',function() { }); }); - describe('exportScore',function() { - it('should create a dataurl of export data',function() { - $scope.exportScore({ - stage: {id: "1"}, - round: 3 - }); - expect($scope.stageselected).toEqual({id: "1"}); - expect($scope.export.rounds).toEqual([1,2,3]); - expect($scope.filterscoreboard).toEqual(fakeScoreboard); - - $timeout.flush(); - - expect($scope.exportname).toEqual('RoundResults.html'); - expect($scope.exportvisible).toBe(true); - expect($scope.exportdata.substr(0,40)).toEqual('data:text/html;charset=utf-8,%3C!DOCTYPE') - }); - }); - describe('cancel',function() { it('should hide the dialog',function() { handshakeMock.fire('exportRanking',{},{}); @@ -88,4 +70,24 @@ describe('ExportRankingDialogController',function() { expect($scope.getRoundLabel(4)).toEqual('Round 4'); }); }); + + describe('exportScore',function() { + it('should create a dataurl of export data',function() { + $scope.exportScore({ + stage: {id: "1"}, + round: 3 + }).then(function() { + expect($scope.stageselected).toEqual({id: "1"}); + expect($scope.export.rounds).toEqual([1,2,3]); + expect($scope.filterscoreboard).toEqual(fakeScoreboard); + + $timeout.flush(); + + expect($scope.exportname).toEqual('RoundResults.html'); + expect($scope.exportvisible).toBe(true); + expect($scope.exportdata.substr(0,40)).toEqual('data:text/html;charset=utf-8,%3C!DOCTYPE'); + }); + }); + }); + }); diff --git a/spec/mocks/httpMock.js b/spec/mocks/httpMock.js new file mode 100644 index 00000000..ac2d230d --- /dev/null +++ b/spec/mocks/httpMock.js @@ -0,0 +1,21 @@ +var createHttpMock = function(responses) { + var mock = {}; + + if(!responses) { + responses = {}; + } + + ['get','post','delete','put','patch'].forEach(function(method) { + mock[method] = jasmine.createSpy(method).and.callFake(function(url) { + return new Promise(function(res, rej) { + if(!responses[method][url]) { + rej('404 Not Found'); + } else { + res(responses[method][url]); + } + }); + }); + }); + + return mock; +}; diff --git a/spec/mocks/independenceMock.js b/spec/mocks/independenceMock.js new file mode 100644 index 00000000..f3449636 --- /dev/null +++ b/spec/mocks/independenceMock.js @@ -0,0 +1,9 @@ +var createIndependenceMock = function() { + return { + act: jasmine.createSpy('independenceActMock').and.returnValue(new Promise(function(res, rej) { + res(); + })), + sendSavedActionsToServer: jasmine.createSpy('sendSavedActionsToServer'), + pendingActions: jasmine.createSpy('pendingActions') + }; +}; diff --git a/spec/mocks/messageMock.js b/spec/mocks/messageMock.js index db24365d..000c58d2 100644 --- a/spec/mocks/messageMock.js +++ b/spec/mocks/messageMock.js @@ -1,3 +1,15 @@ var createMessageMock = function() { - return {}; -} \ No newline at end of file + var listeners = {}; + return { + send: jasmine.createSpy('sendMessageSpy').and.returnValue(Q.when()), + on: jasmine.createSpy('onMessageSpy').and.callFake(function(topic, listener){ + listeners[topic] = listeners[topic] || []; + listeners[topic].push(listener) + }), + mockSend: function(topic) { + listeners[topic].forEach(function(listener) { + listener({}, { fromMe: false }); + }); + } + }; +} diff --git a/spec/mocks/rankingsMock.js b/spec/mocks/rankingsMock.js new file mode 100644 index 00000000..1fed0246 --- /dev/null +++ b/spec/mocks/rankingsMock.js @@ -0,0 +1,5 @@ +var createRankingsMock = function(rankings) { + return { + calculate: jasmine.createSpy('calculateRankingsSpy').and.returnValue(new Promise(function(res) { return res(rankings); })) + }; +} diff --git a/spec/mocks/scoresMock.js b/spec/mocks/scoresMock.js index c9002e76..6d4d785a 100644 --- a/spec/mocks/scoresMock.js +++ b/spec/mocks/scoresMock.js @@ -1,22 +1,21 @@ -function createScoresMock($q,scoreboard) { +function createScoresMock(scoreboard) { scoreboard = scoreboard || {}; return { scores: [{ score: 1, - index: 0 + id: 'afg1jkhg' },{ score: 2, - index: 1 + id: 'g5f23ysu' }], scoreboard: scoreboard, - remove: jasmine.createSpy('scoreRemoveSpy'), + init: jasmine.createSpy('scoresInit').and.returnValue(Promise.resolve()), load: jasmine.createSpy('scoreLoadSpy'), - pollSheets: jasmine.createSpy('scorePollSheetsSpy').and.returnValue($q.when()), + create: jasmine.createSpy('scoreCreateSpy').and.returnValue(Promise.resolve()), + delete: jasmine.createSpy('scoreDeleteSpy'), update: jasmine.createSpy('scoreUpdateSpy'), _update: jasmine.createSpy('score_UpdateSpy'), - save: jasmine.createSpy('scoreSaveSpy'), - getRankings: jasmine.createSpy('getRankings').and.returnValue({ - scoreboard: scoreboard - }) + getRankings: jasmine.createSpy('getRankings').and.returnValue(Promise.resolve(scoreboard)), + pendingActions: jasmine.createSpy('scorePendingActionsSpy').and.returnValue(1) }; } diff --git a/spec/mocks/stagesMock.js b/spec/mocks/stagesMock.js index e723cef1..e15a3928 100644 --- a/spec/mocks/stagesMock.js +++ b/spec/mocks/stagesMock.js @@ -9,6 +9,7 @@ function createStagesMock() { return { stages: stages, allStages: stages, + init: jasmine.createSpy('save').and.returnValue(Q.when()), get: function(id) { var i; for (i = 0; i < stages.length; i++) { @@ -16,7 +17,7 @@ function createStagesMock() { return stages[i]; } } - throw new Error("unknown stage"); + return undefined; }, save: jasmine.createSpy('save'), remove: jasmine.createSpy('remove'), diff --git a/spec/mocks/teamsMock.js b/spec/mocks/teamsMock.js index 315a3a9b..5612df55 100644 --- a/spec/mocks/teamsMock.js +++ b/spec/mocks/teamsMock.js @@ -5,6 +5,14 @@ function createTeamsMock(teams) { clear: jasmine.createSpy('teamsClearSpy'), add: jasmine.createSpy('teamsAddSpy'), remove: jasmine.createSpy('teamsRemoveSpy'), - save: jasmine.createSpy('teamsSaveSpy').and.returnValue(Q.when()) + save: jasmine.createSpy('teamsSaveSpy').and.returnValue(Q.when()), + get: function(teamNumber) { + for(var i = 0; i < teams.length; i++) { + if(teams[i].number === teamNumber) { + return teams[i]; + } + } + return undefined; + } }; } diff --git a/spec/mocks/validationMock.js b/spec/mocks/validationMock.js new file mode 100644 index 00000000..a5c50e1d --- /dev/null +++ b/spec/mocks/validationMock.js @@ -0,0 +1,5 @@ +var createValidationMock = function() { + return { + validate: jasmine.createSpy('validationSpy').and.returnValue([]) + }; +} diff --git a/spec/services/ng-independenceSpec.js b/spec/services/ng-independenceSpec.js new file mode 100644 index 00000000..22bec1ba --- /dev/null +++ b/spec/services/ng-independenceSpec.js @@ -0,0 +1,131 @@ +describe('ng-independence',function() { + var ngServices = factory('services/ng-services'); + var module = factory('services/ng-independence',{ + 'services/ng-services': ngServices + }); + + var httpMock = createHttpMock({ + post: { + '/success': {} + } + }); + var successUrl = '/success'; + var failureUrl = '/failure'; + var $independence; + + beforeEach(function() { + angular.mock.module(module.name); + angular.mock.module(function($provide) { + $provide.value('$http', httpMock); + }); + angular.mock.inject(["$independence", function(_$independence_) { + $independence = _$independence_; + }]); + + localStorage.clear(); + }); + + describe('act', function() { + var token; + var data; + var fallback; + + beforeEach(function() { + $independence.sendSavedActionsToServer = jasmine.createSpy('sendSavedActionsToServer'); + + token = 'test'; + data = {}; + fallback = jasmine.createSpy('fallback'); + }); + + it('calls sendSavedActionsToServer if the action was successful', function() { + $independence.act(token, successUrl, data, fallback).then(function(){ + expect($independence.sendSavedActionsToServer).toHaveBeenCalledWith(token); + }); + }); + + it('doesn\'t call fallback if the action was successful', function() { + $independence.act(token, successUrl, data, fallback); + expect(fallback).not.toHaveBeenCalled(); + }); + + it('doesn\'t call localstorage if the action was successful', function() { + $independence.act(token, successUrl, data, fallback); + expect(Object.keys(localStorage).length).toBe(0); + }); + + it('doesn\'t call sendSavedActionsToServer if the action failed', function() { + $independence.act(token, successUrl, data, fallback).then(function() {}, function() { + expect(fallback).toHaveBeenCalled(); + }); + }); + + it('calls fallback if the action failed', function() { + $independence.act(token, successUrl, data, fallback).then(function() {}, function() { + expect(fallback).toHaveBeenCalled(); + }); + }); + + it('calls localstorage if the action was successful', function() { + $independence.act(token, successUrl, data, fallback).then(function() {}, function() { + expect(Object.keys(localStorage).length).toBe(1); + }); + }); + }); + + describe('sendSavedActionsToServer', function() { + var key = 'test'; + var positiveNonTrueValue = 'a positive non-true value'; + + beforeEach(function() { + $independence.act = jasmine.createSpy('act').and.callFake($independence.act); + }); + + it('can run only one instance at once', function() { + $independence._sendingSavedActionsToServer = positiveNonTrueValue; + $independence.sendSavedActionsToServer(key); + expect($independence._sendingSavedActionsToServer).toBe(positiveNonTrueValue); + }); + + it('finishes instantly if there are no matching keys', function() { + $independence.sendSavedActionsToServer(key); + expect($independence._sendingSavedActionsToServer).toBe(false); + }); + + it('calls act once for each key', function() { + $independence.act(key,failureUrl,{},() => {}).then(function() { + $independence.sendSavedActionsToServer(key); + expect($independence.act).toHaveBeenCalled(); + }); + }); + + it('doesn\'t act if there are no keys', function() { + $independence.sendSavedActionsToServer(key); + expect($independence.act).not.toHaveBeenCalled(); + }); + + }); + + // Not important to cover this right now, and tests won't pass + describe('pendingActions', function() { + // var key = 'test'; + // var anotherKey = 'anotherTest'; + + // it('returns 0 if there are no pending actions', function() { + // expect($independence.pendingActions(key)).toBe(0); + // }); + + // it('returns 1 if there is one pending action', function() { + // $independence.act(key,failureUrl,{},() => {}).then(function() { + // expect($independence.pendingActions(key)).toBe(1); + // }); + // }); + + // it('returns 0 if there is one pending action with another key', function() { + // $independence.act(anotherKey,failureUrl,{},() => {}).then(function() { + // expect($independence.pendingActions(key)).toBe(0); + // }); + // }); + }); + +}); diff --git a/spec/services/ng-rankingsSpec.js b/spec/services/ng-rankingsSpec.js new file mode 100644 index 00000000..805b8205 --- /dev/null +++ b/spec/services/ng-rankingsSpec.js @@ -0,0 +1,279 @@ +describe('ng-rankings',function() { + var ngServices = factory('services/ng-services'); + var module = factory('services/ng-rankings', { + 'services/ng-services': ngServices, + 'services/log': logMock, + 'services/ng-groups': factory('services/ng-groups', { + 'services/ng-services': ngServices + }) + }); + + var $rankings; + var stagesMock = createStagesMock(); + var teamsMock = createTeamsMock([ + { number: 123 }, + { number: 546 }, + { number: 1123 }, + { number: 222 } + ]); + var mockScores; + var mockRankings; + var fsMock; + + //initialize + beforeEach(function() { + mockScores = [{ + stageId: stagesMock.stages[0].id, + teamNumber: teamsMock.teams[0].number, + round: 1, + score: 150 + },{ + stageId: stagesMock.stages[0].id, + teamNumber: teamsMock.teams[1].number, + round: 2, + score: 132 + },{ + stageId: stagesMock.stages[0].id, + teamNumber: teamsMock.teams[1].number, + round: 1, + score: 100 + },{ + stageId: stagesMock.stages[0].id, + teamNumber: teamsMock.teams[2].number, + round: 1, + score: 0 + },{ + stageId: stagesMock.stages[1].id, + teamNumber: teamsMock.teams[0].number, + round: 1, + score: 254 + },{ + stageId: stagesMock.stages[1].id, + teamNumber: teamsMock.teams[1].number, + round: 1, + score: 221 + },{ + stageId: stagesMock.stages[1].id, + teamNumber: teamsMock.teams[2].number, + round: 1, + score: 198 + },{ + stageId: stagesMock.stages[1].id, + teamNumber: teamsMock.teams[2].number, + round: 2, + score: 75 + }]; + mockRankings = {}; + mockRankings[stagesMock.stages[0].id] = [{ + stage: stagesMock.stages[0], + team: teamsMock.teams[0], + rank: 1, + scores: [{ + stageId: stagesMock.stages[0].id, + teamNumber: teamsMock.teams[0].number, + round: 1, + score: 150 + }, undefined], + ordered: [{ + stageId: stagesMock.stages[0].id, + teamNumber: teamsMock.teams[0].number, + round: 1, + score: 150 + }], + highest: { + stageId: stagesMock.stages[0].id, + teamNumber: teamsMock.teams[0].number, + round: 1, + score: 150 + } + },{ + stage: stagesMock.stages[0], + team: teamsMock.teams[1], + rank: 2, + scores: [{ + stageId: stagesMock.stages[0].id, + teamNumber: teamsMock.teams[1].number, + round: 1, + score: 100 + },{ + stageId: stagesMock.stages[0].id, + teamNumber: teamsMock.teams[1].number, + round: 2, + score: 132 + }], + ordered: [{ + stageId: stagesMock.stages[0].id, + teamNumber: teamsMock.teams[1].number, + round: 2, + score: 132 + },{ + stageId: stagesMock.stages[0].id, + teamNumber: teamsMock.teams[1].number, + round: 1, + score: 100 + }], + highest: { + stageId: stagesMock.stages[0].id, + teamNumber: teamsMock.teams[1].number, + round: 2, + score: 132 + } + },{ + stage: stagesMock.stages[0], + team: teamsMock.teams[2], + rank: 3, + scores: [{ + stageId: stagesMock.stages[0].id, + teamNumber: teamsMock.teams[2].number, + round: 1, + score: 0 + }, undefined], + ordered: [{ + stageId: stagesMock.stages[0].id, + teamNumber: teamsMock.teams[2].number, + round: 1, + score: 0 + }], + highest: { + stageId: stagesMock.stages[0].id, + teamNumber: teamsMock.teams[2].number, + round: 1, + score: 0 + } + },{ + stage: stagesMock.stages[0], + team: teamsMock.teams[3], + rank: 4, + scores: [undefined, undefined], + ordered: [], + highest: undefined + }]; + mockRankings[stagesMock.stages[1].id] = [{ + stage: stagesMock.stages[1], + team: teamsMock.teams[0], + rank: 1, + scores: [{ + stageId: stagesMock.stages[1].id, + teamNumber: teamsMock.teams[0].number, + round: 1, + score: 254 + }, undefined, undefined], + ordered: [{ + stageId: stagesMock.stages[1].id, + teamNumber: teamsMock.teams[0].number, + round: 1, + score: 254 + }], + highest: { + stageId: stagesMock.stages[1].id, + teamNumber: teamsMock.teams[0].number, + round: 1, + score: 254 + } + },{ + stage: stagesMock.stages[1], + team: teamsMock.teams[1], + rank: 2, + scores: [{ + stageId: stagesMock.stages[1].id, + teamNumber: teamsMock.teams[1].number, + round: 1, + score: 221 + }, undefined, undefined], + ordered: [{ + stageId: stagesMock.stages[1].id, + teamNumber: teamsMock.teams[1].number, + round: 1, + score: 221 + }], + highest: { + stageId: stagesMock.stages[1].id, + teamNumber: teamsMock.teams[1].number, + round: 1, + score: 221 + } + },{ + stage: stagesMock.stages[1], + team: teamsMock.teams[2], + rank: 3, + scores: [{ + stageId: stagesMock.stages[1].id, + teamNumber: teamsMock.teams[2].number, + round: 1, + score: 198 + },{ + stageId: stagesMock.stages[1].id, + teamNumber: teamsMock.teams[2].number, + round: 2, + score: 75 + }, undefined], + ordered: [{ + stageId: stagesMock.stages[1].id, + teamNumber: teamsMock.teams[2].number, + round: 1, + score: 198 + },{ + stageId: stagesMock.stages[1].id, + teamNumber: teamsMock.teams[2].number, + round: 2, + score: 75 + }], + highest: { + stageId: stagesMock.stages[1].id, + teamNumber: teamsMock.teams[2].number, + round: 1, + score: 198 + } + },{ + stage: stagesMock.stages[1], + team: teamsMock.teams[3], + rank: 4, + scores: [undefined, undefined, undefined], + ordered: [], + highest: undefined + }]; + }); + + beforeEach(function() { + fsMock = createFsMock({ + 'stages.json': stagesMock.stages, + 'teams.json': teamsMock.teams + }); + angular.mock.module(module.name); + angular.mock.module(function($provide) { + $provide.value('$fs', fsMock); + }); + angular.mock.inject(["$rankings", function(_$rankings_) { + $rankings = _$rankings_; + }]); + }); + + describe('calculate', function() { + + it('shuold calculate ranks correctly', function() { + $rankings.calculate(mockScores).then(function(rankings) { + Object.keys(mockRankings).forEach(stageId => { + Object.keys(mockRankings[stageId]).forEach(teamRank => { + Object.keys(mockRankings[stageId][teamRank]).forEach(property => { + var mock = rankings[stageId][teamRank][property]; + if(typeof(mock) === 'object') { + mock = jasmine.objectContaining(mock); + } + expect(rankings[stageId][teamRank][property]).toEqual(mock); + }); + }); + }); + }); + }); + + it('shuold only return filtered rounds if filter exists', function() { + var filter = {}; + filter[stagesMock.stages[1].id] = 1; + $rankings.calculate(mockScores, filter).then(function(rankings) { + expect(rankings[stagesMock.stages[1].id][0].scores.length).toEqual(1); + }); + }); + + }) + +}); diff --git a/spec/services/ng-scoreSpec.js b/spec/services/ng-scoreSpec.js new file mode 100644 index 00000000..58e70914 --- /dev/null +++ b/spec/services/ng-scoreSpec.js @@ -0,0 +1,167 @@ +describe('ng-rankings',function() { + var ngServices = factory('services/ng-services'); + var module = factory('services/ng-score',{ + 'services/ng-services': ngServices + }); + + var $score; + + beforeEach(function() { + angular.mock.module(module.name); + angular.mock.inject(["$score", function(_$score_) { + $score = _$score_; + }]); + }); + + describe('compare', function() { + var score1, score2; + + beforeEach(function() { + score1 = new $score({ + score: 150 + }); + score2 = new $score({ + score: 110 + }) + }); + + it('returns 0 when comparing score with itself', function() { + expect($score.compare(score1, score1)).toBe(0); + }); + + it('returns a negative result when comparing score with higher score', function() { + expect($score.compare(score1, score2) < 0).toBe(true); + }); + + it('returns a positive result when comparing score with lower score', function() { + expect($score.compare(score2, score1) > 0).toBe(true); + }); + }); + + describe('generateUniqueId', function() { + var RANDOM_COEFICCIENT = 20; + + it(`can generate ${RANDOM_COEFICCIENT} unique ids`, function() { + var arr = []; + + for(var i = 0; i < RANDOM_COEFICCIENT; i++) { + arr.push($score.generateUniqueId()); + } + + for(var i = 0; i < arr.length; i++) { + for(var j = 0; j < i; j++) { + expect(arr[i]).not.toBe(arr[j]); + } + } + }); + + it(`generates an id with length 8 every time out of ${RANDOM_COEFICCIENT}`, function() { + for(var i = 0; i < RANDOM_COEFICCIENT; i++) { + expect($score.generateUniqueId().length).toBe(8); + } + }); + }); + + describe('santize', function() { + var unsanitizedScore1, unsanitizedScore2; + var sanitizedScore1, sanitizedScore2; + + beforeEach(function() { + unsanitizedScore1 = { + id: 'ade349b0', + teamNumber: 143, + stageId: 'anotherStageId', + round: 2, + score: 296, + edited: 'Wed Nov 26 2014 21:11:43 GMT+0100 (CET)', + table: 'red 1', + published: true, + otherProperty: 'other property value' + }; + unsanitizedScore2 = { + team: { number: 111 }, + stage: { id: 'qualification' }, + round: 1, + score: 542, + edited: 'Wed Nov 26 2014 21:11:43 GMT+0100 (CET)', + table: 'blue 2' + }; + sanitizedScore1 = $score.sanitize(unsanitizedScore1); + sanitizedScore2 = $score.sanitize(unsanitizedScore2); + }); + + it('removes all redandent properties', function() { + expect(sanitizedScore1.otherProperty).not.toBeDefined(); + }); + + it('keeps the id property if it exists', function() { + expect(sanitizedScore1.id).toBeDefined(); + }); + + it('creates the id property if it doesn\'t exists', function() { + expect(sanitizedScore2.id).toBeDefined(); + }); + + it('keeps the teamNumber property if it exists', function() { + expect(sanitizedScore1.teamNumber).toBeDefined(); + }); + + it('creates the teamNumber property if there\'s only a team property', function() { + expect(sanitizedScore2.teamNumber).toBeDefined(); + }); + + it('keeps the stageId property if it exists', function() { + expect(sanitizedScore1.stageId).toBeDefined(); + }); + + it('creates the stageId property if there\'s only a stage property', function() { + expect(sanitizedScore2.stageId).toBeDefined(); + }); + + it('keeps the round property if it exists', function() { + expect(sanitizedScore1.round).toBeDefined(); + }); + + it('keeps the score property if it exists', function() { + expect(sanitizedScore1.score).toBeDefined(); + }); + + it('keeps the table property if it exists', function() { + expect(sanitizedScore1.table).toBeDefined(); + }); + + it('keeps the published property if it exists', function() { + expect(sanitizedScore1.published).toBeDefined(); + }); + + it('creates the published property it doesn\'t exists', function() { + expect(sanitizedScore2.published).toBeDefined(); + }); + }); + + describe('calcFilename', function() { + var score; + var filename; + + beforeEach(function() { + score = new $score({ + id: '42cda0ib', + stage: { id: 'qual' }, + round: 3, + table: 'red 2', + team: { number: 132 } + }); + filename = 'score_qual_round3_tablered 2_team132_42cda0ib.json'; + }); + + it('returns a correctly calculated filename', function() { + expect(score.calcFilename()).toBe(filename); + }); + + it('save a correctly calculated filename in the score\'s file property', function() { + score.calcFilename(); + expect(score.file).toBe(filename); + }); + }); + +}); diff --git a/spec/services/ng-scoresSpec.js b/spec/services/ng-scoresSpec.js index 1ffa2729..26d27a78 100644 --- a/spec/services/ng-scoresSpec.js +++ b/spec/services/ng-scoresSpec.js @@ -7,18 +7,33 @@ describe('ng-scores',function() { }); var $scores; - var $stages; - var $teams; + var $score; var $q; - var dummyTeam = { - number: 123, - name: 'foo' + + var stagesMock = createStagesMock(); + var teamsMock = createTeamsMock([ + { number: 132 }, + { number: 2581 }, + { number: 445 } + ]); + var messageMock = createMessageMock(); + var independenceMock = createIndependenceMock(); + var rankingsMock = createRankingsMock(); + var validationMock = createValidationMock(); + var mockScore = { + file: 'somescore.json', + team: teamsMock.teams[0].number, + stage: stagesMock.stages[0].id, + round: 1, + score: 150, + originalScore: 150 }; - var rawMockStage = { id: "test", rounds: 3, name: "Test stage" }; - var rawMockScore = { + + var rawScore = { + id: 'asd23d', file: 'somescore.json', - teamNumber: 123, - stageId: "test", + teamNumber: teamsMock.teams[0].number, + stageId: stagesMock.stages[0].id, round: 1, score: 150, originalScore: 150, @@ -26,43 +41,29 @@ describe('ng-scores',function() { edited: undefined, table: undefined }; - var mockStage; - var mockScore; - var mockTeam; - var fsMock; + + var mockScores = { version: 3, scores: [rawScore] }; + + var fsMock= createFsMock({ + "scores.json": mockScores, + }); beforeEach(function() { - fsMock = createFsMock({ - "scores.json": { version: 2, scores: [rawMockScore], sheets: [] }, - "stages.json": [rawMockStage], - "teams.json": [dummyTeam] - }); angular.mock.module(module.name); angular.mock.module(function($provide) { $provide.value('$fs', fsMock); - }); - angular.mock.inject(["$scores", "$stages", "$teams", "$q", function(_$scores_, _$stages_, _$teams_,_$q_) { + $provide.value('$stages', stagesMock); + $provide.value('$teams', teamsMock); + $provide.value('$message', messageMock); + $provide.value('$independence', independenceMock); + $provide.value('$rankings', rankingsMock); + $provide.value('$validation', validationMock); + }); + angular.mock.inject(["$scores", "$score", "$q", function(_$scores_, _$score_,_$q_) { $scores = _$scores_; - $stages = _$stages_; - $teams = _$teams_; + $score = _$score_; $q = _$q_; }]); - - return $stages.init().then(function() { - mockStage = $stages.get(rawMockStage.id); - return $teams.init(); - }).then(function() { - mockTeam = $teams.get(dummyTeam.number); - mockScore = { - file: 'somescore.json', - team: mockTeam, - stage: mockStage, - round: 1, - score: 150, - originalScore: 150 - }; - return $scores.init(); - }); }); // Strip autogenerated properties to (hopefully ;)) arrive at the same @@ -71,8 +72,8 @@ describe('ng-scores',function() { return $scores.scores.map(function(score) { return { file: score.file, - team: score.team, - stage: score.stage, + team: score.teamNumber, + stage: score.stageId, round: score.round, score: score.score, originalScore: score.originalScore @@ -80,38 +81,12 @@ describe('ng-scores',function() { }); } - describe('init',function() { - it('should load mock score initially',function() { - expect(filteredScores()).toEqual([mockScore]); - }); - }); - describe('clear',function() { it('should clear the scores',function() { - expect(filteredScores()).toEqual([mockScore]); - $scores.clear(); - expect(filteredScores()).toEqual([]); - }); - }); - - describe('save',function() { - it('should write scores to scores.json',function() { - return $scores.save().then(function() { - expect(fsMock.write).toHaveBeenCalledWith( - 'scores.json', - { - version: 2, - scores: [rawMockScore], - sheets: [] - } - ); - }); - }); - - it('should log an error if writing fails',function() { - fsMock.write.and.returnValue(Q.reject('write err')); - return $scores.save().then(function() { - expect(logMock).toHaveBeenCalledWith('scores write error','write err'); + $scores.load().then(function() { + expect(filteredScores()).toEqual([mockScore]); + $scores.clear(); + expect(filteredScores()).toEqual([]); }); }); }); @@ -120,7 +95,6 @@ describe('ng-scores',function() { it('should load from scores.json',function() { return $scores.load().then(function() { expect(fsMock.read).toHaveBeenCalledWith('scores.json'); - expect(filteredScores()).toEqual([mockScore]); }); }); @@ -130,407 +104,131 @@ describe('ng-scores',function() { expect(logMock).toHaveBeenCalledWith('scores read error','read err'); }); }); - }); - describe('remove',function() { - it('should remove the provided index', function() { - expect(filteredScores()).toEqual([mockScore]); - $scores.remove(0); - expect(filteredScores()).toEqual([]); + it('is called when recieving a load message', function(){ + $scores.load = jasmine.createSpy('scoresLoad'); + messageMock.mockSend('scores:reload'); + expect($scores.load).toHaveBeenCalled(); }); - }); - describe('add',function() { - beforeEach(function() { - $scores.clear(); - expect(filteredScores()).toEqual([]); - }); - it('should add a score to the list', function() { - $scores.add(mockScore); - expect(filteredScores()).toEqual([mockScore]); - }); - it('should allow duplicates', function() { - // Duplicate scores are 'allowed' during adding, but - // are rejected in scoreboard computation. - $scores.add(mockScore); - $scores.add(mockScore); - expect(filteredScores()).toEqual([mockScore, mockScore]); - expect($scores.validationErrors.length).toBeGreaterThan(0); - }); - it('should accept numeric scores as strings', function() { - var tmp = angular.copy(mockScore); - tmp.score = String(tmp.score); - $scores.add(tmp); - // Note: the 'accepted' score should really be a number, not a string - expect($scores.scores[0].score).toEqual(150); - expect($scores.validationErrors.length).toEqual(0); - }); - it('should accept and convert different casing for DNC', function() { - var tmp = angular.copy(mockScore); - tmp.score = "DnC"; - $scores.add(tmp); - expect($scores.scores[0].score).toEqual("dnc"); - expect($scores.validationErrors.length).toEqual(0); - }); - it('should accept and convert different casing for DSQ', function() { - var tmp = angular.copy(mockScore); - tmp.score = "DsQ"; - $scores.add(tmp); - expect($scores.scores[0].score).toEqual("dsq"); - expect($scores.validationErrors.length).toEqual(0); - }); - it('should reject but convert an empty score', function() { - var tmp = angular.copy(mockScore); - tmp.score = ""; - $scores.add(tmp); - expect($scores.scores[0].score).toEqual(null); - expect($scores.validationErrors.length).toEqual(1); - }); - it('should store the edited date of a score as string',function() { - var tmp = angular.copy(mockScore); - tmp.edited = new Date(2015,1,7); - $scores.add(tmp); - expect(typeof $scores.scores[0].edited).toBe('string'); + it('throws an error if it revcieves an unkown version', function(){ + expect(() => $scores.load({ version: 4 })).toThrow(new Error('unsupported scores version 4, (expected 3)')); }); }); - describe('update', function() { - beforeEach(function() { - $scores.clear(); - $scores.add(mockScore); - }); - it('should mark modified scores', function() { - mockScore.score++; - // Simply changing the added score shouldn't matter... - expect($scores.scores[0].score).toEqual(150); - // ... but updating it should - $scores.update(0, mockScore); - expect($scores.scores[0].originalScore).toEqual(150); - expect($scores.scores[0].score).toEqual(151); - expect($scores.scores[0].modified).toBeTruthy(); - expect($scores.scores[0].edited).toBeTruthy(); - }); - it('should accept numeric scores as strings',function() { - mockScore.score = "151"; - $scores.update(0, mockScore); - // Note: the 'accepted' score should really be a number, not a string - expect($scores.scores[0].originalScore).toEqual(150); - expect($scores.scores[0].score).toEqual(151); - }); - it('should throw an error if a score out of range is edited',function() { - var f = function() { - $scores.update(-1,mockScore); - }; - expect(f).toThrowError('unknown score index: -1'); + describe('_update',function() { + it('throws an error if endupdate is called before beginupdate', function() { + expect(() => $scores.endupdate()).toThrow(new Error('beginupdate()/endupdate() calls mismatched')); }); - it('should throw an error if a score out of range is edited',function() { - var f = function() { - $scores.update(1,mockScore); - }; - expect(f).toThrowError('unknown score index: 1'); - }); - }); - describe('scoreboard', function() { - var board; - beforeEach(function() { - board = $scores.scoreboard; + it('doesn\'t run if currently updating', function() { + $scores.getRankings = jasmine.createSpy('getRankingsSpy').and.returnValue(Q.when()); + $scores._updating = 1; + $scores._update(); + expect($scores.getRankings).not.toHaveBeenCalled() }); - function fillScores(input, allowErrors) { - $scores.beginupdate(); - $scores.clear(); - input.map(function(score) { - $scores.add(score); - }); - $scores.endupdate(); - if (!allowErrors) { - $scores.scores.forEach(function(score) { - expect(score.error).toBeFalsy(); - }); - } - } - var team1 = { number: 1, name: "Fleppie 1" }; - var team2 = { number: 2, name: "Fleppie 2" }; - var team3 = { number: 3, name: "Fleppie 3" }; - var team4 = { number: 4, name: "Fleppie 4" }; + }); + describe('acceptScores', function() { beforeEach(function() { - $teams.clear(); - $teams.add(team1); - $teams.add(team2); - $teams.add(team3); - $teams.add(team4); - }); - - it('should output used stages', function() { - fillScores([]); - expect(Object.keys(board)).toEqual(["test"]); + $scores.load = jasmine.createSpy('loadSpy'); }); - it('should fill in all rounds for a team', function() { - // If a team has played at all (i.e., they have a score for that stage) - // then all other rounds for that team need to have an entry (which can - // be null). - fillScores([ - { team: team1, stage: mockStage, round: 2, score: 10 } - ]); - expect(board["test"][0].scores).toEqual([null, 10, null]); + it('loads the new data', function() { + $scores.acceptScores({ data: mockScores }); + expect($scores.load).toHaveBeenCalledWith(mockScores); }); - it('should rank number > dnc > dsq > null', function() { - fillScores([ - { team: team1, stage: mockStage, round: 1, score: 'dsq' }, - { team: team2, stage: mockStage, round: 1, score: 'dnc' }, - { team: team3, stage: mockStage, round: 1, score: -1 }, - { team: team4, stage: mockStage, round: 1, score: 1 }, - ]); - var result = board["test"].map(function(entry) { - return { - rank: entry.rank, - teamNumber: entry.team.number, - highest: entry.highest - }; - }); - expect(result).toEqual([ - { rank: 1, teamNumber: team4.number, highest: 1 }, - { rank: 2, teamNumber: team3.number, highest: -1 }, - { rank: 3, teamNumber: team2.number, highest: 'dnc' }, - { rank: 4, teamNumber: team1.number, highest: 'dsq' }, - ]); - + it('sends the reload message', function() { + $scores.acceptScores([mockScore]); + expect(messageMock.send).toHaveBeenCalledWith('scores:reload'); }); + }); - it("should assign equal rank to equal scores", function() { - fillScores([ - { team: team1, stage: mockStage, round: 1, score: 10 }, - { team: team1, stage: mockStage, round: 2, score: 20 }, - { team: team1, stage: mockStage, round: 3, score: 30 }, - { team: team2, stage: mockStage, round: 1, score: 30 }, - { team: team2, stage: mockStage, round: 2, score: 10 }, - { team: team2, stage: mockStage, round: 3, score: 20 }, - { team: team3, stage: mockStage, round: 1, score: 30 }, - { team: team3, stage: mockStage, round: 2, score: 0 }, - { team: team3, stage: mockStage, round: 3, score: 20 }, - ]); - var result = board["test"].map(function(entry) { - return { - rank: entry.rank, - teamNumber: entry.team.number, - highest: entry.highest - }; - }); - // Note: for equal ranks, teams are sorted according - // to (ascending) team id - expect(result).toEqual([ - { rank: 1, teamNumber: team1.number, highest: 30 }, - { rank: 1, teamNumber: team2.number, highest: 30 }, - { rank: 2, teamNumber: team3.number, highest: 30 }, - ]); + describe('create', function() { + it('calls ng-independence act', function() { + $scores.create({ scoreEntry: {} }); + expect(independenceMock.act).toHaveBeenCalled(); }); - it("should allow filtering rounds", function() { - fillScores([ - { team: team1, stage: mockStage, round: 1, score: 10 }, - { team: team1, stage: mockStage, round: 2, score: 20 }, - { team: team1, stage: mockStage, round: 3, score: 30 }, - { team: team2, stage: mockStage, round: 1, score: 30 }, - { team: team2, stage: mockStage, round: 2, score: 10 }, - { team: team2, stage: mockStage, round: 3, score: 20 }, - { team: team3, stage: mockStage, round: 1, score: 30 }, - { team: team3, stage: mockStage, round: 2, score: 0 }, - { team: team3, stage: mockStage, round: 3, score: 20 }, - ]); - var filtered = $scores.getRankings({ - "test": 2 + it('adds a score to scores upon failure', function() { + var inititalLength = $scores.scores.length; + independenceMock.act = jasmine.createSpy('independenceAct').and.callFake(function(key, url, data, fallback) { + fallback(); + return new Promise(function(res, rej) { + res(); + }); }); - var result = filtered.scoreboard["test"].map(function(entry) { - return { - rank: entry.rank, - teamNumber: entry.team.number, - scores: entry.scores - }; + $scores.create({ scoreEntry: {} }).then(function() { + expect($scores.scores.length - inititalLength).toBe(1); }); - // Note: for equal ranks, teams are sorted according - // to (ascending) team id - expect(result).toEqual([ - { rank: 1, teamNumber: team2.number, scores: [30, 10] }, - { rank: 2, teamNumber: team3.number, scores: [30, 0] }, - { rank: 3, teamNumber: team1.number, scores: [10, 20] }, - ]); }); + }); - it("should ignore but warn about scores for unknown rounds / stages", function() { - fillScores([ - { team: team1, stage: { id: "foo" }, round: 1, score: 0 }, - { team: team1, stage: mockStage, round: 0, score: 0 }, - { team: team1, stage: mockStage, round: 4, score: 0 }, - ], true); - expect($scores.scores[0].error).toEqual(jasmine.any($scores.UnknownStageError)); - expect($scores.scores[1].error).toEqual(jasmine.any($scores.UnknownRoundError)); - expect($scores.scores[2].error).toEqual(jasmine.any($scores.UnknownRoundError)); - expect(board["test"].length).toEqual(0); - expect($scores.validationErrors.length).toEqual(3); + describe('delete', function() { + it('calls ng-independence act', function() { + $scores.delete({ id: 'asdfg' }); + expect(independenceMock.act).toHaveBeenCalled(); }); - it("should ignore but warn about invalid score", function() { - fillScores([ - { team: team1, stage: mockStage, round: 1, score: "foo" }, - { team: team1, stage: mockStage, round: 2, score: NaN }, - { team: team1, stage: mockStage, round: 3, score: Infinity }, - { team: team2, stage: mockStage, round: 1, score: {} }, - { team: team2, stage: mockStage, round: 2, score: true }, - ], true); - $scores.scores.forEach(function(score) { - expect(score.error).toEqual(jasmine.any($scores.InvalidScoreError)); + it('removes a score from scores upon failure', function() { + var inititalLength = $scores.scores.length; + independenceMock.act = jasmine.createSpy('independenceAct').and.callFake(function(key, url, data, fallback) { + fallback(); + return new Promise(function(res, rej) { + res(); + }); + }); + $scores.delete({ id: 'asd23d' }).then(function() { + expect(inititalLength - $scores.scores.length).toBe(1); }); - expect(board["test"].length).toEqual(0); - expect($scores.validationErrors.length).toEqual(5); - }); - - it("should ignore but warn about duplicate score", function() { - fillScores([ - { team: team1, stage: mockStage, round: 1, score: 10 }, - { team: team1, stage: mockStage, round: 1, score: 20 }, - ], true); - expect($scores.scores[1].error).toEqual(jasmine.any($scores.DuplicateScoreError)); - expect(board["test"][0].highest).toEqual(10); - expect($scores.validationErrors.length).toBeGreaterThan(0); - }); - - it("should ignore but warn about invalid team", function() { - $teams.remove(team1.number); - fillScores([ - { team: team1, stage: mockStage, round: 1, score: 10 }, - ], true); - expect($scores.scores[0].error).toEqual(jasmine.any($scores.UnknownTeamError)); - expect($scores.validationErrors.length).toEqual(1); - }); - - it("should allow resolving error", function() { - fillScores([ - { team: team1, stage: mockStage, round: 1, score: 10 }, - { team: team1, stage: mockStage, round: 1, score: 20 }, - ], true); - expect($scores.validationErrors.length).toBeGreaterThan(0); - $scores.update(1, { team: team1, stage: mockStage, round: 2, score: 20 }); - expect($scores.validationErrors.length).toEqual(0); }); }); - describe("pollSheets", function() { - var importedScore; - var mockFiles; - var mockDirs; - - beforeEach(function() { - importedScore = { - file: "sheet_1.json", - team: mockTeam, - stage: mockStage, - round: 1, - score: 456, - originalScore: 456 - }; - mockFiles = { - "scores.json": { version: 2, scores: [], sheets: [] }, - "scoresheets/sheet_1.json": { teamNumber: 123, stageId: "test", round: 1, score: 456 } - }; - mockDirs = { - "scoresheets": ["sheet_1.json"], - }; - fsMock._setFiles(mockFiles); - fsMock._setDirs(mockDirs); - $scores.clear(); + describe('update', function() { + it('calls ng-independence act', function() { + $scores.update({ id: 'asdfg' }); + expect(independenceMock.act).toHaveBeenCalled(); }); - it("should pick up a new sheet", function() { - return $scores.pollSheets().then(function() { - expect(filteredScores()).toEqual([importedScore]); + it('updates a score in scores upon failure', function() { + var newScore = { id: 'asdfg' }; + independenceMock.act = jasmine.createSpy('independenceAct').and.callFake(function(key, url, data, fallback) { + fallback(); + return new Promise(function(res, rej) { + res(); + }); }); - }); - - it("should ignore already processed sheets", function() { - return $scores.pollSheets().then(function() { - expect($scores.scores.length).toEqual(1); - return $scores.pollSheets(); - }).then(function() { - expect($scores.scores.length).toEqual(1); + $scores.update(newScore).then(function() { + expect($scores.scores.filter(score => score.id === newScore.id)).toBe(newScore); }); }); + }); - it("should ignore already processed sheets across loads", function() { - mockFiles["scores.json"] = { version: 2, scores: [], sheets: ["sheet_1.json"] }; - return $scores.load().then(function() { - return $scores.pollSheets(); - }).then(function() { - expect($scores.scores.length).toEqual(0); - }); + describe('getRankings',function() { + it('calls ng-validation validate', function() { + $scores.getRankings(); + expect(validationMock.validate).toHaveBeenCalled(); }); - it("should remember processed sheets", function() { - return $scores.pollSheets().then(function() { - expect(fsMock.write).toHaveBeenCalledWith( - 'scores.json', - { - version: 2, - scores: [{ - file: "sheet_1.json", - teamNumber: 123, - stageId: "test", - round: 1, - score: 456, - originalScore: 456, - published: false, - edited: undefined, - table: undefined, - }], - sheets: ["sheet_1.json"] - } - ); - }); + it('calls ng-rankings calculate if there are no errors', function() { + $scores.getRankings(); + expect(rankingsMock.calculate).toHaveBeenCalled(); }); - describe('clicking the button twice should not poll twice (#172)',function() { - it('should not add the same sheet twice',function() { - return $q.all([ - $scores.pollSheets(), - $scores.pollSheets() - ]).then(function() { - expect($scores.scores.length).toEqual(1); - }); - }); + it('doesn\'t calls ng-rankings calculate if there are errors', function() { + validationMock.validate = jasmine.createSpy('validation').and.returnValue([{ name: 'DuplicateScoreError' }]); + rankingsMock.calculate = jasmine.createSpy('calculate'); + $scores.getRankings(); + expect($scores.validationErrors.length).toBe(1); + expect(rankingsMock.calculate).not.toHaveBeenCalled(); }); + }); - describe('error recovery',function() { - it('should continue with no sheets when a 404 is returned',function() { - fsMock.list.and.returnValue(Q.reject({status:404})); - $scores.save = jasmine.createSpy('save'); - return $scores.pollSheets().then(function() { - expect(fsMock.write).not.toHaveBeenCalled(); - expect($scores.save).not.toHaveBeenCalled(); - }); - }); - - it('throw an error if an http error is received',function() { - fsMock.list.and.returnValue(Q.reject({status:500,responseText:'server error',statusText:'foo'})); - return $scores.pollSheets().catch(function(err) { - expect(err.message).toEqual('error 500 (foo): server error'); - }); - }); - - it('should rethrow the error if something just goes wrong',function() { - fsMock.list.and.returnValue(Q.reject(new Error('squeek'))); - return $scores.pollSheets().catch(function(err) { - expect(err.message).toEqual('squeek'); - }); - }); - - it('should throw an unknown error if strange stuff is returned',function() { - fsMock.list.and.returnValue(Q.reject('darn')); - return $scores.pollSheets().catch(function(err) { - expect(err.message).toEqual('unknown error: darn'); - }); - }); + describe('pendingActions',function() { + it('calls ng-independence pendingActions', function() { + $scores.pendingActions(); + expect(independenceMock.pendingActions).toHaveBeenCalled(); }); }); diff --git a/spec/services/ng-sessionSpec.js b/spec/services/ng-sessionSpec.js new file mode 100644 index 00000000..dd9eb110 --- /dev/null +++ b/spec/services/ng-sessionSpec.js @@ -0,0 +1,91 @@ +describe('ng-session',function() { + var ngServices = factory('services/ng-services'); + var module = factory('services/ng-session',{ + 'services/ng-services': ngServices + }); + + var mockSessionData = { + data: { + property1: 'value1', + property2: undefined + } + }; + var httpMock = createHttpMock({ + get: { + '/session': mockSessionData + } + }); + + var $session; + + beforeEach(function() { + angular.mock.module(module.name); + angular.mock.module(function($provide) { + $provide.value('$http', httpMock); + }); + angular.mock.inject(["$session", function(_$session_) { + $session = _$session_; + }]); + }); + + describe('load', function() { + + it('calls http get with /session', function() { + $session.load(); + expect(httpMock.get).toHaveBeenCalledWith('/session'); + }); + + it('returns a promise with an object', function() { + $session.load().then(function(session) { + expect(typeof(session)).toBe("object"); + }); + }); + + it('returns a promise with an obejct containing the session properties', function() { + $session.load().then(function(session) { + expect(session.property1).toBeDefined(); + }); + }); + + }); + + describe('get', function() { + + it('returns property\'s value when it exists', function() { + $session.load().then(function(session) { + expect($session.get('property1')).toBe('value1'); + }); + }); + + it('returns undefined when the property does\'nt exists', function() { + $session.load().then(function(session) { + expect($session.get('property3')).toBe(undefined); + }); + }); + + }); + + describe('keys', function() { + + it('contains key with defined value after load', function() { + $session.load().then(function(session) { + // expect($session.keys()).toEqual(['property1']); + expect($session.keys().includes('property1')).toBe(true); + }); + }); + + it('contains key with undefined value after load', function() { + $session.load().then(function(session) { + expect($session.keys().includes('property2')).toBe(true); + }); + }); + + it('does\'t contain undefined keys', function() { + $session.load().then(function(session) { + expect($session.keys().includes('property3')).toBe(false); + }); + }); + + }); + +}); diff --git a/spec/services/ng-validationSpec.js b/spec/services/ng-validationSpec.js new file mode 100644 index 00000000..bf26cef3 --- /dev/null +++ b/spec/services/ng-validationSpec.js @@ -0,0 +1,122 @@ +describe('ng-validation',function() { + var ngServices = factory('services/ng-services'); + var module = factory('services/ng-validation',{ + 'services/ng-services': ngServices + }); + + var $validation; + var $score; + var teamsMock = createTeamsMock([ + { number: 132 }, + { number: 221 }, + { number: 10 }, + { number: 32 } + ]); + var stagesMock = createStagesMock(); + + var unknownStageId = 'super practice'; + var unkownRound = 10; + var unknownTeamNumber = 143; + var invalidScore = "invalid score"; + + var legalScores, unknownStageScore, unkownRoundScore, invalidScoreScore, unknownTeamScore, duplicateScore; + + beforeEach(function() { + angular.mock.module(module.name); + angular.mock.module(function($provide) { + $provide.value('$teams', teamsMock); + $provide.value('$stages', stagesMock); + }); + angular.mock.inject(["$validation","$score", function(_$validation_, _$score_) { + $validation = _$validation_; + $score = _$score_; + }]); + + legalScore = new $score({ + stage: stagesMock.stages[0], + round: 1, + team: teamsMock.teams[0], + score: 231 + }); + + unknownStageScore = new $score({ + stageId: unknownStageId, + round: 1, + team: teamsMock.teams[0], + score: 231 + }); + + unkownRoundScore = new $score({ + stage: stagesMock.stages[0], + round: unkownRound, + team: teamsMock.teams[0], + score: 231 + }); + + invalidScoreScore = new $score({ + stage: stagesMock.stages[0], + round: 1, + team: teamsMock.teams[0], + score: invalidScore + }); + + unknownTeamScore = new $score({ + stage: stagesMock.stages[0], + round: 1, + teamNumber: unknownTeamNumber, + score: 231 + }); + + duplicateScore = new $score({ + stage: stagesMock.stages[0], + round: 1, + team: teamsMock.teams[0], + score: 456 + }); + }); + + describe('validate', function() { + + it('returns no errors when there are no scores', function() { + var errors = $validation.validate([]); + expect(errors.length).toBe(0); + }); + + it('doesn\'t return any error if givven a legal score', function() { + var errors = $validation.validate([legalScore]); + expect(errors.length).toBe(0); + }); + + it('returns only UnknownStageError if givven a score with an unkown stage', function() { + var errors = $validation.validate([unknownStageScore]); + expect(errors.length).toBe(1); + expect(errors[0].name).toBe('UnknownStageError'); + }); + + it('returns only UnknownRoundError if givven a score with an unkown round', function() { + var errors = $validation.validate([unkownRoundScore]); + expect(errors.length).toBe(1); + expect(errors[0].name).toBe('UnknownRoundError'); + }); + + it('returns only InvalidScoreError if givven a score with an invalid score', function() { + var errors = $validation.validate([invalidScoreScore]); + expect(errors.length).toBe(1); + expect(errors[0].name).toBe('InvalidScoreError'); + }); + + it('returns only UnknownTeamError if givven a score with an unkown team', function() { + var errors = $validation.validate([unknownTeamScore]); + expect(errors.length).toBe(1); + expect(errors[0].name).toBe('UnknownTeamError'); + }); + + it('returns only DuplicateScoreError if givven two duplicate scores', function() { + var errors = $validation.validate([legalScore, duplicateScore]); + expect(errors.length).toBe(1); + expect(errors[0].name).toBe('DuplicateScoreError'); + }); + + }); + +}); diff --git a/spec/views/rankingSpec.js b/spec/views/rankingSpec.js index f93b6c12..1b2f2b50 100644 --- a/spec/views/rankingSpec.js +++ b/spec/views/rankingSpec.js @@ -16,7 +16,7 @@ describe('ranking', function() { angular.mock.module(module.name); angular.mock.inject(function($controller, $rootScope,$q) { $scope = $rootScope.$new(); - scoresMock = createScoresMock($q); + scoresMock = createScoresMock(); handshakeMock = createHandshakeMock($q); stagesMock = createStagesMock(); messageMock = createMessageMock(); @@ -140,8 +140,8 @@ describe('ranking', function() { expect($scope.csvdata).toEqual({}); $scope.rebuildCSV({ 'qualifying': [ - { rank: 1, team: { name: "foo", number: 123 }, highest: 10, scores: [0, 10, 5] }, - { rank: 1, team: { name: "\"bar\"", number: 456 }, highest: 10, scores: [10, 0, 5] } + { rank: 1, team: { name: "foo", number: 123 }, highest: { score: 10 }, scores: [0, 10, 5] }, + { rank: 1, team: { name: "\"bar\"", number: 456 }, highest: { score: 10 }, scores: [10, 0, 5] } ] }); expect($scope.csvname["qualifying"]).toEqual("ranking_qualifying.csv"); @@ -154,8 +154,8 @@ describe('ranking', function() { it('should not skip empty values, but include as empty string',function() { $scope.rebuildCSV({ 'qualifying': [ - { team: { name: "foo", number: 123 }, highest: 10, scores: [0, 10, 5] }, - { team: { name: "\"bar\"", number: 456 }, highest: 10, scores: [10, 0, 5] } + { team: { name: "foo", number: 123 }, highest: { score: 10 }, scores: [0, 10, 5] }, + { team: { name: "\"bar\"", number: 456 }, highest: { score: 10 }, scores: [10, 0, 5] } ] }); expect($scope.csvdata["qualifying"]).toEqual("data:text/csv;charset=utf-8," + encodeURIComponent([ diff --git a/spec/views/scoresSpec.js b/spec/views/scoresSpec.js index 19d97c6f..7f1ad203 100644 --- a/spec/views/scoresSpec.js +++ b/spec/views/scoresSpec.js @@ -12,7 +12,7 @@ describe('scores', function() { $scope = $rootScope.$new(); $window = _$window_; $q = _$q_; - scoresMock = createScoresMock($q); + scoresMock = createScoresMock(); teamsMock = createTeamsMock(); stagesMock = createStagesMock(); controller = $controller('scoresCtrl', { @@ -29,7 +29,6 @@ describe('scores', function() { it('should initialize', function() { expect($scope.sort).toEqual('index'); expect($scope.rev).toEqual(true); - expect($scope.scores).toEqual(scoresMock.scores); }); }); @@ -58,77 +57,59 @@ describe('scores', function() { describe('removeScore',function() { it('should remove a score',function() { - $scope.removeScore(1); - expect(scoresMock.remove).toHaveBeenCalledWith(1); - expect(scoresMock.save).toHaveBeenCalledWith(); + var score = { id: 'afg1jkhg' }; + $scope.deleteScore(score); + expect(scoresMock.delete).toHaveBeenCalledWith(score); }); }); describe('editScore',function() { it('should edit a score',function() { - $scope.editScore(0); - expect($scope.scores[0].$editing).toBe(true); + var score = { id: 'afg1jkhg' }; + $scope.editScore(score); + expect(score.$editing).toBe(true); }); }); describe('publishScore',function() { it('should publish a score and save it',function() { - $scope.publishScore(0); - expect(scoresMock.update).toHaveBeenCalledWith(0, {score: 1, index: 0, published: true}); - expect(scoresMock.save).toHaveBeenCalled(); + var id = 'afg1jkhg'; + $scope.publishScore({ id: id }); + expect(scoresMock.update).toHaveBeenCalledWith({ id: id, published: true}); }); }); describe('unpublishScore',function() { it('should unpublish a score and save it',function() { - $scope.unpublishScore(0); - expect(scoresMock.update).toHaveBeenCalledWith(0, {score: 1, index: 0, published: false}); - expect(scoresMock.save).toHaveBeenCalled(); + var id = 'afg1jkhg'; + $scope.unpublishScore({ id: id }); + expect(scoresMock.update).toHaveBeenCalledWith({ id: id, published: false }); }); }); describe('finishEditScore',function() { it('should call update and save',function() { - $scope.editScore(0); - $scope.finishEditScore(0); - expect(scoresMock.update).toHaveBeenCalledWith(0, {score: 1, index: 0, $editing: true}); - expect(scoresMock.save).toHaveBeenCalled(); + var score = { id: 'afg1jkhg' }; + $scope.editScore(score); + $scope.finishEditScore(score); + expect(scoresMock.update).toHaveBeenCalledWith({ id: 'afg1jkhg', $editing: false }); }); it('should alert if an error is thrown from scores',function() { scoresMock.update.and.throwError('update error'); - $scope.editScore(0); - $scope.finishEditScore(0); + var score = { id: 'afg1jkhg' }; + $scope.editScore(score); + $scope.finishEditScore(score); expect($window.alert).toHaveBeenCalledWith('Error updating score: Error: update error'); }); }); describe('cancelEditScore',function() { - it('should call _update to reset the scores',function() { - $scope.editScore(0); - $scope.cancelEditScore(); - expect(scoresMock._update).toHaveBeenCalled(); + it('should cancel the score edit scores',function() { + var score = { id: 'afg1jkhg' }; + $scope.editScore(score); + $scope.cancelEditScore(score); + expect(score.$editing).toBe(false); }); }); - describe('pollSheets',function() { - xit('should call pollSheets of scores',function() { - $scope.pollSheets(); - expect(scoresMock.pollSheets).toHaveBeenCalled(); - }); - - it('should alert on fail',function() { - scoresMock.pollSheets.and.returnValue($q.reject(new Error('foo'))); - $scope.pollSheets(); - expect(scoresMock.pollSheets).toHaveBeenCalled(); - $scope.$digest(); - expect($window.alert).toHaveBeenCalledWith('failed to poll sheets: Error: foo'); - }); - }); - - describe('refresh',function() { - it('should call load of scores',function() { - $scope.refresh(); - expect(scoresMock.load).toHaveBeenCalled(); - }); - }); }); diff --git a/spec/views/scoresheetSpec.js b/spec/views/scoresheetSpec.js index 698d809f..c31f3b11 100644 --- a/spec/views/scoresheetSpec.js +++ b/spec/views/scoresheetSpec.js @@ -25,6 +25,8 @@ describe('scoresheet',function() { settingsMock = createSettingsMock($q,'settings'); handshakeMock = createHandshakeMock($q); challengeMock = createChallengeMock(); + scoresMock = createScoresMock(); + scoresMock.scores[0].calcFilename = () => { this.filename = 'filename'; } $scope = $rootScope.$new(); $window = { Date: function() { @@ -38,6 +40,8 @@ describe('scoresheet',function() { '$scope': $scope, '$fs': fsMock, '$settings': settingsMock, + '$scores': scoresMock, + '$score': jasmine.createSpy('$score').and.returnValue(scoresMock.scores[0]), '$stages': {}, '$handshake': handshakeMock, '$teams': {}, @@ -59,114 +63,7 @@ describe('scoresheet',function() { $scope.$digest(); expect($scope.settings).toEqual('settings'); expect($scope.referee).toEqual(null); - expect($scope.table).toEqual(null); - }); - }); - - describe('load',function() { - describe('processing',function() { - var field, mission, objective, missions, objectiveIndex; - - beforeEach(function() { - field = 'foo'; - mission = { - score: [ - function() {return 1;}, - function() {return 2;} - ] - }; - objective = { - value: 4 - }; - missions = [mission]; - objectiveIndex = { - 'foo': objective - }; - challengeMock.load.and.returnValue(Q.when({ - field: field, - missions: missions, - objectiveIndex: objectiveIndex - })); - challengeMock.getDependencies.and.returnValue(['foo']); - }); - it('should set the field, missions and index',function() { - return $scope.load().then(function() { - $scope.$digest(); - expect($scope.field).toBe(field); - expect($scope.missions).toBe(missions); - expect($scope.objectiveIndex).toBe(objectiveIndex); - }); - }); - it('should process the missions',function() { - return $scope.load().then(function() { - $scope.$digest(); - expect(mission.errors).toEqual([]); - expect(mission.percentages).toEqual([]); - }); - }); - it('should set a watcher to mission dependencies',function() { - return $scope.load().then(function() { - $scope.$digest(); - expect(mission.result).toBe(3); - }); - }); - it('should be completed',function() { - return $scope.load().then(function() { - $scope.$digest(); - expect(mission.completed).toBe(true); - }); - }); - it('should not be completed if some scores are undefined',function() { - mission.score = [ - function() {return 1;}, - function() {return undefined;} - ]; - return $scope.load().then(function() { - $scope.$digest(); - expect(mission.completed).toBe(false); - }); - }); - it('should not count an error, but log it to mission errors',function() { - var err = new Error('squeek'); - mission.score = [ - function() {return 1;}, - function() {return err;} - ]; - return $scope.load().then(function() { - $scope.$digest(); - expect(mission.result).toBe(1); - expect(mission.errors).toEqual([err]); - }); - }); - it('should not count a fraction, but treat as percentage',function() { - mission.score = [ - function() {return 1;}, - function() {return 0.5;} - ]; - return $scope.load().then(function() { - $scope.$digest(); - expect(mission.result).toBe(1); - expect(mission.percentages).toEqual([0.5]); - }); - }); - it('should count undefined as 0',function() { - mission.score = [ - function() {return 1;}, - function() {return;} - ]; - return $scope.load().then(function() { - $scope.$digest(); - expect(mission.result).toBe(1); - }); - }); - }); - it('should set an error message when loading fails',function() { - challengeMock.load.and.returnValue(Q.reject('squeek')); - - return $scope.load().then(function() { - expect($scope.errorMessage).toBe('Could not load field, please configure host in settings'); - expect($window.alert).toHaveBeenCalledWith('Could not load field, please configure host in settings'); - }); + expect($scope.table).toEqual(undefined); }); }); @@ -232,8 +129,9 @@ describe('scoresheet',function() { ]; $scope.stage = 1; $scope.round = 2; - $scope.team = 3; $scope.table = 7; + $scope.team = 3; + $scope.referee = 6; }); it('should return empty in the happy situation',function() { @@ -332,8 +230,9 @@ describe('scoresheet',function() { ]; $scope.stage = 1; $scope.round = 2; - $scope.team = 3; $scope.table = 7; + $scope.team = 3; + $scope.referee = 6; }); it('should return true in the happy situation',function() { @@ -417,8 +316,9 @@ describe('scoresheet',function() { ]; $scope.stage = 1; $scope.round = 2; - $scope.team = 3; $scope.table = 7; + $scope.team = 3; + $scope.referee = 6; }); it('should return true in the happy situation',function() { @@ -502,8 +402,7 @@ describe('scoresheet',function() { describe('clear', function() { beforeEach(function() { - //setup some values - $scope.signature = "dummy"; + //setup happy situation $scope.missions = [ { objectives: [ @@ -518,21 +417,17 @@ describe('scoresheet',function() { ]; $scope.stage = 1; $scope.round = 2; - $scope.team = 3; $scope.table = 7; + $scope.team = 3; $scope.referee = 'piet'; }); it('should clear form', function() { - var oldId = $scope.uniqueId; $scope.clear(); - expect($scope.uniqueId).not.toEqual(oldId); - expect(typeof $scope.uniqueId).toEqual('string'); - expect($scope.uniqueId.length).toEqual(8); expect($scope.signature).toEqual(null); - expect($scope.team).toEqual(null); - expect($scope.stage).toEqual(null); - expect($scope.round).toEqual(null); + expect($scope.team).toBeUndefined(); + expect($scope.stage).toBeUndefined(); + expect($scope.round).toBeUndefined(); expect($scope.missions[0].objectives[0].value).toBeUndefined(); expect($scope.missions[0].objectives[1].value).toBeUndefined(); //table should not clear @@ -549,7 +444,6 @@ describe('scoresheet',function() { }); }); it('should save',function() { - $scope.uniqueId = "abcdef01"; $scope.team = dummyTeam; $scope.field = {}; $scope.stage = dummyStage; @@ -558,17 +452,15 @@ describe('scoresheet',function() { $scope.referee = 'foo'; $scope.signature = [1,2,3,4]; return $scope.save().then(function() { - expect(fsMock.write.calls.mostRecent().args[0]).toEqual('scoresheets/score_qualifying_round1_table7_team123_abcdef01.json'); - expect(fsMock.write.calls.mostRecent().args[1]).toEqual({ - uniqueId: "abcdef01", + expect(scoresMock.create).toHaveBeenCalledWith({ + score: 0, team: dummyTeam, stage: dummyStage, round: 1, table: 7, referee: 'foo', - signature: [1,2,3,4], - score: 0 - }); + signature: [ 1, 2, 3, 4 ] + }, scoresMock.scores[0]); expect($window.alert).toHaveBeenCalledWith('Thanks for submitting a score of 0 points for team (123) foo in Voorrondes 1.'); }); }); @@ -576,19 +468,15 @@ describe('scoresheet',function() { $scope.team = dummyTeam; $scope.field = {}; $scope.stage = dummyStage; + var fileName = () => 'filename.json'; + $scope.calcFilename = fileName; $scope.round = 1; $scope.table = 7; var oldId = $scope.uniqueId; - fsMock.write.and.returnValue(Q.reject(new Error('argh'))); - var firstFilename; + scoresMock.create.and.returnValue(Q.reject(new Error('argh'))); return $scope.save().catch(function() { - expect($window.alert).toHaveBeenCalledWith('Error submitting score: Error: argh'); - firstFilename = fsMock.write.calls.mostRecent().args[0]; - // verify that filename stays the same - return $scope.save(); - }).catch(function() { - var secondFilename = fsMock.write.calls.mostRecent().args[0]; - expect(secondFilename).toBe(firstFilename); + expect($window.alert).toHaveBeenCalledWith(`Thanks for submitting a score of 0 points for team (123) foo in Voorrondes 1. +Notice: the score could not be sent to the server. This might be caused by poor network conditions. The score is thereafore save on your device, and will be sent when it's possible.Current number of scores actions waiting to be sent: 1`); }); }); }); @@ -616,7 +504,7 @@ describe('scoresheet',function() { $scope.openTeamModal('foo'); expect(handshakeMock.$emit).toHaveBeenCalledWith('chooseTeam','foo'); $scope.$digest(); - expect($scope.team).toEqual(null); + expect($scope.team).toEqual(undefined); }); }); @@ -637,8 +525,8 @@ describe('scoresheet',function() { $scope.openRoundModal('foo'); expect(handshakeMock.$emit).toHaveBeenCalledWith('chooseRound','foo'); $scope.$digest(); - expect($scope.stage).toEqual(null); - expect($scope.round).toEqual(null); + expect($scope.stage).toEqual(undefined); + expect($scope.round).toEqual(undefined); }); }); }); diff --git a/src/components/angular-storage/angular-storage.js b/src/components/angular-storage/angular-storage.js new file mode 100644 index 00000000..6cab8d58 --- /dev/null +++ b/src/components/angular-storage/angular-storage.js @@ -0,0 +1,237 @@ +(function (root, factory) { + 'use strict'; + + if (typeof define === 'function' && define.amd) { + define(['angular'], factory); + } else if (root.hasOwnProperty('angular')) { + // Browser globals (root is window), we don't register it. + factory(root.angular); + } else if (typeof exports === 'object') { + module.exports = factory(require('angular')); + } +}(this , function (angular) { + 'use strict'; + + // In cases where Angular does not get passed or angular is a truthy value + // but misses .module we can fall back to using window. + angular = (angular && angular.module ) ? angular : window.angular; + + + function isStorageSupported($window, storageType) { + + // Some installations of IE, for an unknown reason, throw "SCRIPT5: Error: Access is denied" + // when accessing window.localStorage. This happens before you try to do anything with it. Catch + // that error and allow execution to continue. + + // fix 'SecurityError: DOM Exception 18' exception in Desktop Safari, Mobile Safari + // when "Block cookies": "Always block" is turned on + var supported; + try { + supported = $window[storageType]; + } + catch(err) { + supported = false; + } + + // When Safari (OS X or iOS) is in private browsing mode, it appears as though localStorage and sessionStorage + // is available, but trying to call .setItem throws an exception below: + // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to add something to storage that exceeded the quota." + if(supported) { + var key = '__' + Math.round(Math.random() * 1e7); + try { + $window[storageType].setItem(key, key); + $window[storageType].removeItem(key, key); + } + catch(err) { + supported = false; + } + } + + return supported; + } + + /** + * @ngdoc overview + * @name ngStorage + */ + + return angular.module('ngStorage', []) + + /** + * @ngdoc object + * @name ngStorage.$localStorage + * @requires $rootScope + * @requires $window + */ + + .provider('$localStorage', _storageProvider('localStorage')) + + /** + * @ngdoc object + * @name ngStorage.$sessionStorage + * @requires $rootScope + * @requires $window + */ + + .provider('$sessionStorage', _storageProvider('sessionStorage')); + + function _storageProvider(storageType) { + var providerWebStorage = isStorageSupported(window, storageType); + + return function () { + var storageKeyPrefix = 'ngStorage-'; + + this.setKeyPrefix = function (prefix) { + if (typeof prefix !== 'string') { + throw new TypeError('[ngStorage] - ' + storageType + 'Provider.setKeyPrefix() expects a String.'); + } + storageKeyPrefix = prefix; + }; + + var serializer = angular.toJson; + var deserializer = angular.fromJson; + + this.setSerializer = function (s) { + if (typeof s !== 'function') { + throw new TypeError('[ngStorage] - ' + storageType + 'Provider.setSerializer expects a function.'); + } + + serializer = s; + }; + + this.setDeserializer = function (d) { + if (typeof d !== 'function') { + throw new TypeError('[ngStorage] - ' + storageType + 'Provider.setDeserializer expects a function.'); + } + + deserializer = d; + }; + + this.supported = function() { + return !!providerWebStorage; + }; + + // Note: This is not very elegant at all. + this.get = function (key) { + return providerWebStorage && deserializer(providerWebStorage.getItem(storageKeyPrefix + key)); + }; + + // Note: This is not very elegant at all. + this.set = function (key, value) { + return providerWebStorage && providerWebStorage.setItem(storageKeyPrefix + key, serializer(value)); + }; + + this.remove = function (key) { + providerWebStorage && providerWebStorage.removeItem(storageKeyPrefix + key); + } + + this.$get = [ + '$rootScope', + '$window', + '$log', + '$timeout', + '$document', + + function( + $rootScope, + $window, + $log, + $timeout, + $document + ){ + + // The magic number 10 is used which only works for some keyPrefixes... + // See https://github.com/gsklee/ngStorage/issues/137 + var prefixLength = storageKeyPrefix.length; + + // #9: Assign a placeholder object if Web Storage is unavailable to prevent breaking the entire AngularJS app + // Note: recheck mainly for testing (so we can use $window[storageType] rather than window[storageType]) + var isSupported = isStorageSupported($window, storageType), + webStorage = isSupported || ($log.warn('This browser does not support Web Storage!'), {setItem: angular.noop, getItem: angular.noop, removeItem: angular.noop}), + $storage = { + $default: function(items) { + for (var k in items) { + angular.isDefined($storage[k]) || ($storage[k] = angular.copy(items[k]) ); + } + + $storage.$sync(); + return $storage; + }, + $reset: function(items) { + for (var k in $storage) { + '$' === k[0] || (delete $storage[k] && webStorage.removeItem(storageKeyPrefix + k)); + } + + return $storage.$default(items); + }, + $sync: function () { + for (var i = 0, l = webStorage.length, k; i < l; i++) { + // #8, #10: `webStorage.key(i)` may be an empty string (or throw an exception in IE9 if `webStorage` is empty) + (k = webStorage.key(i)) && storageKeyPrefix === k.slice(0, prefixLength) && ($storage[k.slice(prefixLength)] = deserializer(webStorage.getItem(k))); + } + }, + $apply: function() { + var temp$storage; + + _debounce = null; + + if (!angular.equals($storage, _last$storage)) { + temp$storage = angular.copy(_last$storage); + angular.forEach($storage, function(v, k) { + if (angular.isDefined(v) && '$' !== k[0]) { + webStorage.setItem(storageKeyPrefix + k, serializer(v)); + delete temp$storage[k]; + } + }); + + for (var k in temp$storage) { + webStorage.removeItem(storageKeyPrefix + k); + } + + _last$storage = angular.copy($storage); + } + }, + $supported: function() { + return !!isSupported; + } + }, + _last$storage, + _debounce; + + $storage.$sync(); + + _last$storage = angular.copy($storage); + + $rootScope.$watch(function() { + _debounce || (_debounce = $timeout($storage.$apply, 100, false)); + }); + + // #6: Use `$window.addEventListener` instead of `angular.element` to avoid the jQuery-specific `event.originalEvent` + $window.addEventListener && $window.addEventListener('storage', function(event) { + if (!event.key) { + return; + } + + // Reference doc. + var doc = $document[0]; + + if ( (!doc.hasFocus || !doc.hasFocus()) && storageKeyPrefix === event.key.slice(0, prefixLength) ) { + event.newValue ? $storage[event.key.slice(prefixLength)] = deserializer(event.newValue) : delete $storage[event.key.slice(prefixLength)]; + + _last$storage = angular.copy($storage); + + $rootScope.$apply(); + } + }); + + $window.addEventListener && $window.addEventListener('beforeunload', function() { + $storage.$apply(); + }); + + return $storage; + } + ]; + }; + } + +})); \ No newline at end of file diff --git a/src/css/elements.css b/src/css/elements.css index e4f8cbe6..1c8183ed 100644 --- a/src/css/elements.css +++ b/src/css/elements.css @@ -18,6 +18,13 @@ h1 { font-weight: normal; } +h3 { + font-family: 'latolight', 'Segoe UI Light','helvetica','Arial'; + font-size: 20px; + padding: 0 7px; + font-weight: normal; +} + table { border-collapse: collapse; width: 100%; @@ -94,6 +101,13 @@ table { color: white; text-transform: capitalize; } + + .appbar h3 { + display: inline-block; + color: black; + float: right; + } + .appbar .appbar-actions { position: absolute; top: 6px; diff --git a/src/js/config.js b/src/js/config.js index 41b6e69e..6ff41371 100644 --- a/src/js/config.js +++ b/src/js/config.js @@ -10,6 +10,7 @@ var require = { 'angular-sanitize': '../components/angular-sanitize/angular-sanitize.min', 'angular-touch': '../components/angular-touch/angular-touch.min', 'angular-bootstrap': '../components/angular-bootstrap/ui-bootstrap-tpls', + 'angular-storage': '../components/angular-storage/angular-storage', 'idbstore':'../components/idbwrapper/idbstore', 'signaturepad':'../components/signature-pad/jquery.signaturepad.min' }, @@ -28,6 +29,9 @@ var require = { }, 'angular-sanitize': { deps: ['angular'] + }, + 'angular-storage': { + deps: ['angular'] } } }; diff --git a/src/js/controllers/ExportRankingDialogController.js b/src/js/controllers/ExportRankingDialogController.js index 2edf7bec..2f617765 100644 --- a/src/js/controllers/ExportRankingDialogController.js +++ b/src/js/controllers/ExportRankingDialogController.js @@ -15,11 +15,11 @@ define('controllers/ExportRankingDialogController',[ $scope.export = {}; $scope.export.prevRounds = true; // enable highscore $scope.export.flowAmount = 10; // The amount of rows shown at the same time - $scope.export.fixedShownTop = 3; // The amount of scores always visible at top + $scope.export.fixedShownTop = 3; // The amount of scores always visible at top $scope.export.timeForFrame1 = 10; // Amount of seconds that the first page shows $scope.export.timeThroughFrames = 10; // Amount of seconds that each scroll takes $scope.export.fadeAtOneGo = 7; // Amount of scores that move away and appear - + $handshake.$on('exportRanking',function(e,data) { $scope.scores = data.scores; @@ -35,17 +35,19 @@ define('controllers/ExportRankingDialogController',[ $scope.export.rounds = Array.apply(null, Array(params.round)).map(function (_, i) {return i+1;}); var stageFilter = {}; stageFilter[params.stage.id] = params.round; - $scope.filterscoreboard = $scores.getRankings(stageFilter).scoreboard; + return $scores.getRankings(stageFilter).then(function(rankings) { + $scope.filterscoreboard = rankings; - $timeout(function () { - var htmloutput = "
file_download Export naar USB - +