Uses angular-locker to store settings in localStorage

- settings.js now has its own module : 'jamstash.settings.controller'. This makes it easier to test it and identify what dependencies it has.
- Renames 'jamstash.settings' module into 'jamstash.settings.service'
- Adds an angular constant to hold the current Jamstash version in app.js.
- Adds a way to upgrade incrementally what was in localStorage : in 4.4.5, DefaultSearchType will no longer be an object but an int, so we must init it with an int value, otherwise a blank option will be displayed. We detect what version we are using and what version was stored using persistence-service.js and run the upgrade accordingly.

- Refactors almost completely persistence-service_test.js
- Unit-tests some of settings.js's methods.
This commit is contained in:
Hyzual 2015-03-22 21:48:50 +01:00
parent f98740d613
commit a97e5159bc
17 changed files with 383 additions and 150 deletions

View file

@ -1,8 +1,8 @@
'use strict';
/* Declare app level module */
angular.module('JamStash', ['ngCookies', 'ngRoute', 'ngSanitize', 'ui.keypress',
'jamstash.subsonic.controller', 'jamstash.archive.controller', 'jamstash.player.controller', 'jamstash.queue.controller', 'jamstash.persistence'])
angular.module('JamStash', ['ngCookies', 'ngRoute', 'ngSanitize', 'ui.keypress', 'angular-underscore/utils',
'jamstash.subsonic.controller', 'jamstash.archive.controller', 'jamstash.player.controller', 'jamstash.queue.controller', 'jamstash.settings.controller', 'jamstash.persistence'])
.config(['$routeProvider',function($routeProvider) {
$routeProvider
@ -48,4 +48,6 @@ angular.module('JamStash', ['ngCookies', 'ngRoute', 'ngSanitize', 'ui.keypress',
}
};
}]);
}]);
}])
.constant('jamstashVersion', '4.4.5');

View file

@ -3,7 +3,7 @@
*
* Access Archive.org
*/
angular.module('jamstash.archive.service', ['jamstash.settings', 'jamstash.model', 'jamstash.notifications',
angular.module('jamstash.archive.service', ['jamstash.settings.service', 'jamstash.model', 'jamstash.notifications',
'jamstash.player.service'])
.factory('archive', ['$rootScope', '$http', '$q', '$sce', 'globals', 'model', 'utils', 'map', 'notifications', 'player',

View file

@ -32,19 +32,12 @@ angular.module('JamStash')
$scope.loadSettings = function () {
// Temporary Code to Convert Cookies added 2/2/2014
if ($cookieStore.get('Settings')) {
utils.setValue('Settings', $cookieStore.get('Settings'), false);
persistence.saveSettings($cookieStore.get('Settings'));
$cookieStore.remove('Settings');
}
if (utils.getValue('Settings')) {
$.each(utils.getValue('Settings'), function (k, v) {
if (v == 'false') { v = false; }
if (v == 'true') { v = true; }
var exclude = ['Url'];
var idx = exclude.indexOf(k);
if (idx === -1) {
globals.settings[k] = v;
}
});
var settings = persistence.getSettings();
if (settings !== undefined) {
globals.settings = _(settings).omit('Url');
}
if (utils.getValue("SavedCollections")) { globals.SavedCollections = utils.getValue("SavedCollections").split(","); }
if (utils.getValue("DefaultCollection")) { globals.DefaultCollection = utils.getValue("DefaultCollection"); }

View file

@ -21,7 +21,14 @@ describe("Main controller", function() {
player.queue = [];
// Mock the persistence service
persistence = jasmine.createSpyObj("persistence", ["loadQueue", "loadTrackPosition", "getVolume", "saveVolume"]);
persistence = jasmine.createSpyObj("persistence", [
"loadQueue",
"loadTrackPosition",
"getVolume",
"saveVolume",
"getSettings",
"saveSettings"
]);
inject(function (_$controller_, $rootScope, _$document_, _$window_, _$location_, _$cookieStore_, _utils_, globals, _model_, _notifications_, _Page_) {
scope = $rootScope.$new();
@ -166,6 +173,24 @@ describe("Main controller", function() {
expect(player.previousTrack).not.toHaveBeenCalled();
});
});
describe("loadSettings() -", function() {
it("Given user settings were saved using persistence, when I load the settings, the globals object will be completed with them, excluding the Url setting", function() {
persistence.getSettings.and.returnValue({
"Url": "http://gmelinite.com/contrastive/hypercyanotic?a=overdrive&b=chirpling#postjugular",
"Username": "Hollingshead",
"AutoPlaylistSize": 25,
"AutoPlay": true
});
scope.loadSettings();
expect(mockGlobals.settings.Username).toEqual("Hollingshead");
expect(mockGlobals.settings.AutoPlaylistSize).toBe(25);
expect(mockGlobals.settings.AutoPlay).toBe(true);
expect(mockGlobals.settings.Url).toBeUndefined();
});
});
});
describe("When starting up,", function() {

View file

@ -3,7 +3,7 @@
*
* Set the page's title from anywhere, the angular way
*/
angular.module('jamstash.page', ['jamstash.settings', 'jamstash.utils'])
angular.module('jamstash.page', ['jamstash.settings.service', 'jamstash.utils'])
.factory('Page', ['$interval', 'globals', 'utils', function($interval, globals, utils){
'use strict';

View file

@ -5,7 +5,8 @@
* Provides load, save and delete operations for the current song and queue.
* Data storage provided by HTML5 localStorage.
*/
angular.module('jamstash.persistence', ['jamstash.settings', 'jamstash.player.service', 'jamstash.notifications', 'angular-locker'])
angular.module('jamstash.persistence', ['angular-locker',
'jamstash.settings.service', 'jamstash.player.service', 'jamstash.notifications'])
.config(['lockerProvider', function (lockerProvider) {
lockerProvider.setDefaultDriver('local')
@ -13,7 +14,9 @@ angular.module('jamstash.persistence', ['jamstash.settings', 'jamstash.player.se
.setEventsEnabled(false);
}])
.service('persistence', ['globals', 'player', 'notifications', 'locker', function (globals, player, notifications, locker) {
.service('persistence', ['globals', 'player', 'notifications', 'locker', 'jamstashVersion',
function (globals, player, notifications, locker, jamstashVersion) {
/* Manage current track */
this.loadTrackPosition = function () {
// Load Saved Song
var song = locker.get('CurrentSong');
@ -33,6 +36,7 @@ angular.module('jamstash.persistence', ['jamstash.settings', 'jamstash.player.se
if (globals.settings.Debug) { console.log('Removing Current Position from localStorage'); }
};
/* Manage playing queue */
this.loadQueue = function () {
// load Saved queue
var queue = locker.get('CurrentQueue');
@ -55,6 +59,7 @@ angular.module('jamstash.persistence', ['jamstash.settings', 'jamstash.player.se
if (globals.settings.Debug) { console.log('Removing Play Queue from localStorage'); }
};
/* Manage player volume */
this.getVolume = function () {
return locker.get('Volume');
};
@ -66,5 +71,37 @@ angular.module('jamstash.persistence', ['jamstash.settings', 'jamstash.player.se
this.deleteVolume = function () {
locker.forget('Volume');
};
/* Manage user settings */
this.getSettings = function () {
if(this.getVersion() !== jamstashVersion) {
this.upgradeToVersion(jamstashVersion);
}
return locker.get('Settings');
};
this.saveSettings = function (settings) {
locker.put('Settings', settings);
};
this.deleteSettings = function () {
locker.forget('Settings');
};
/* Manage Jamstash Version */
this.getVersion = function () {
return locker.get('version');
};
this.upgradeToVersion = function (version) {
locker.put('version', version);
switch (version) {
case '4.4.5':
var settings = locker.get('Settings');
settings.DefaultSearchType = 0;
this.saveSettings(settings);
break;
}
};
}]);

View file

@ -1,10 +1,16 @@
describe("Persistence service", function() {
'use strict';
var persistence, player, notifications, locker;
var song;
var persistence, player, notifications, locker,
song, fakeStorage;
beforeEach(function() {
module('jamstash.persistence');
module('jamstash.persistence', function ($provide) {
// Mock locker
$provide.decorator('locker', function () {
return jasmine.createSpyObj("locker", ["get", "put", "forget"]);
});
$provide.constant("jamstashVersion", "1.0.1");
});
inject(function (_persistence_, _player_, _notifications_, _locker_) {
persistence = _persistence_;
@ -20,24 +26,20 @@ describe("Persistence service", function() {
album: 'Tammanyize'
};
player.queue = [];
});
describe("load from localStorage -", function() {
var fakeStorage;
beforeEach(function() {
fakeStorage = {};
spyOn(locker, "get").and.callFake(function(key) {
locker.get.and.callFake(function(key) {
return fakeStorage[key];
});
});
describe("loadTrackPosition -", function() {
describe("loadTrackPosition() -", function() {
beforeEach(function() {
spyOn(player, "load");
});
it("Given that we previously saved the current track's position in local Storage, it loads the song we saved into the player", function() {
it("Given a previously saved song in local storage, when I load the song, the player will load it", function() {
fakeStorage = { 'CurrentSong': song };
persistence.loadTrackPosition();
@ -46,14 +48,24 @@ describe("Persistence service", function() {
expect(player.load).toHaveBeenCalledWith(song);
});
it("Given that we didn't save anything in local Storage, it doesn't load anything", function() {
it("Given that no song was previously saved in local storage, it doesn't do anything", function() {
persistence.loadTrackPosition();
expect(locker.get).toHaveBeenCalledWith('CurrentSong');
expect(player.load).not.toHaveBeenCalled();
});
});
describe("loadQueue -", function() {
it("saveTrackPosition() - saves the current track's position in local storage", function() {
persistence.saveTrackPosition(song);
expect(locker.put).toHaveBeenCalledWith('CurrentSong', song);
});
it("deleteTrackPosition() - deletes the current track from local storage", function() {
persistence.deleteTrackPosition();
expect(locker.forget).toHaveBeenCalledWith('CurrentSong');
});
describe("loadQueue() -", function() {
beforeEach(function() {
spyOn(notifications, "updateMessage");
spyOn(player, "addSongs").and.callFake(function (songs) {
@ -62,7 +74,7 @@ describe("Persistence service", function() {
});
});
it("Given that we previously saved the playing queue in local Storage, it fills the player's queue with what we saved and notifies the user", function() {
it("Given a previously saved queue in local storage, when I load the queue, the player's queue will be filled with the retrieved queue and the user will be notified", function() {
var queue = [
{ id: 8705 },
{ id: 1617 },
@ -77,7 +89,7 @@ describe("Persistence service", function() {
expect(notifications.updateMessage).toHaveBeenCalledWith('3 Saved Song(s)', true);
});
it("Given that we didn't save anything in local Storage, it doesn't load anything", function() {
it("Given that no queue was previously saved in local storage, when I load the queue, the player's queue will stay the same and no notification will be displayed", function() {
persistence.loadQueue();
expect(locker.get).toHaveBeenCalledWith('CurrentQueue');
@ -86,36 +98,7 @@ describe("Persistence service", function() {
});
});
describe("getVolume -", function() {
it("Given that we previously saved the volume in local Storage, it retrieves it", function() {
fakeStorage = { 'Volume': 0.46582 };
var volume = persistence.getVolume();
expect(locker.get).toHaveBeenCalledWith('Volume');
expect(volume).toBe(0.46582);
});
it("Given that we didn't save the volume in local Storage, it returns undefined", function() {
var volume = persistence.getVolume();
expect(locker.get).toHaveBeenCalledWith('Volume');
expect(volume).toBeUndefined();
});
});
});
describe("save to localStorage -", function() {
beforeEach(function() {
spyOn(locker, "put");
});
it("saves the current track's position in local Storage", function() {
persistence.saveTrackPosition(song);
expect(locker.put).toHaveBeenCalledWith('CurrentSong', song);
});
it("saves the playing queue in local Storage", function() {
it("saveQueue() - saves the playing queue in local storage", function() {
player.queue = [
{ id: 1245 },
{ id: 7465 },
@ -125,30 +108,114 @@ describe("Persistence service", function() {
expect(locker.put).toHaveBeenCalledWith('CurrentQueue', player.queue);
});
it("saves the volume in local Storage", function() {
persistence.saveVolume(0.05167);
expect(locker.put).toHaveBeenCalledWith('Volume', 0.05167);
});
});
describe("remove from localStorage -", function() {
beforeEach(function() {
spyOn(locker, "forget");
});
it("deletes the current track from local Storage", function() {
persistence.deleteTrackPosition();
expect(locker.forget).toHaveBeenCalledWith('CurrentSong');
});
it("deletes the saved playing queue from local Storage", function() {
it("deleteQueue() - deletes the saved playing queue from local storage", function() {
persistence.deleteQueue();
expect(locker.forget).toHaveBeenCalledWith('CurrentQueue');
});
it("deletes the saved volume from local Storage", function() {
describe("getVolume() -", function() {
it("Given a previously saved volume value in local storage, it retrieves it", function() {
fakeStorage = { 'Volume': 0.46582 };
var volume = persistence.getVolume();
expect(locker.get).toHaveBeenCalledWith('Volume');
expect(volume).toBe(0.46582);
});
it("Given that no volume value was previously saved in local storage, it returns undefined", function() {
var volume = persistence.getVolume();
expect(locker.get).toHaveBeenCalledWith('Volume');
expect(volume).toBeUndefined();
});
});
it("saveVolume() - given a volume, it will be saved in local storage", function() {
persistence.saveVolume(0.05167);
expect(locker.put).toHaveBeenCalledWith('Volume', 0.05167);
});
it("deleteVolume() - deletes the saved volume from local storage", function() {
persistence.deleteVolume();
expect(locker.forget).toHaveBeenCalledWith('Volume');
});
describe("getSettings() -", function() {
it("Given previously saved user settings in local storage, it retrieves them", function() {
fakeStorage = {
"Settings": {
"url": "https://headed.com/aleurodidae/taistrel?a=roquet&b=trichophoric#cathole",
"Username": "Haupert"
}
};
var settings = persistence.getSettings();
expect(locker.get).toHaveBeenCalledWith('Settings');
expect(settings).toEqual({
"url": "https://headed.com/aleurodidae/taistrel?a=roquet&b=trichophoric#cathole",
"Username": "Haupert"
});
});
it("Given that the previously stored Jamstash version was '1.0.0' and given the current constant jamstash.version was '1.0.1', when I get the settings, then upgradeToVersion will be called", function() {
spyOn(persistence, 'upgradeToVersion');
persistence.getSettings();
expect(persistence.upgradeToVersion).toHaveBeenCalledWith('1.0.1');
});
it("Given that no user settings had been saved in local storage, it returns undefined", function() {
var settings = persistence.getSettings();
expect(locker.get).toHaveBeenCalledWith('Settings');
expect(settings).toBeUndefined();
});
});
it("saveSettings() - given a user settings object, it will be saved in local storage", function() {
persistence.saveSettings({
"url": "http://crotalic.com/cabernet/coelenteron?a=dayshine&b=pantaletless#sus",
"Username": "Herrig"
});
expect(locker.put).toHaveBeenCalledWith('Settings', {
"url": "http://crotalic.com/cabernet/coelenteron?a=dayshine&b=pantaletless#sus",
"Username": "Herrig"
});
});
it("deleteSettings() - deletes the saved user settings from local storage", function() {
persistence.deleteSettings();
expect(locker.forget).toHaveBeenCalledWith('Settings');
});
describe("upgradeToVersion() -", function() {
describe("Given that Jamstash version '1.0.0' was previously stored in local storage,", function() {
beforeEach(function() {
fakeStorage.version = '1.0.0';
});
it("when I upgrade the storage version to '1.0.1', Jamstash version '1.0.1' will be in local storage", function() {
persistence.upgradeToVersion('1.0.1');
expect(locker.put).toHaveBeenCalledWith('version', '1.0.1');
});
it("that settings.DefaultSearchType was stored as an object and that a changeset for version '4.4.5' was defined that changes it to an int, when I upgrade the storage version to '4.4.5', settings.DefaultSearch will be stored as an int", function() {
fakeStorage = {
Settings: {
DefaultSearchType: {
id: "song",
name: "Song"
}
}
};
persistence.upgradeToVersion('4.4.5');
expect(locker.put).toHaveBeenCalledWith('Settings', {
DefaultSearchType: 0
});
});
});
});
});

View file

@ -3,7 +3,7 @@
*
* Provides generally useful functions, like sorts, date-related functions, localStorage access, etc.
*/
angular.module('jamstash.utils', ['jamstash.settings'])
angular.module('jamstash.utils', ['jamstash.settings.service'])
.service('utils', ['$rootScope', 'globals', function ($rootScope, globals) {
'use strict';

View file

@ -4,7 +4,7 @@
* Encapsulates the jPlayer plugin. It watches the player service for the song to play, load or restart.
* It also enables jPlayer to attach event handlers to our UI through css Selectors.
*/
angular.module('jamstash.player.directive', ['jamstash.player.service', 'jamstash.settings', 'jamstash.subsonic.service', 'jamstash.notifications', 'jamstash.persistence', 'jamstash.page'])
angular.module('jamstash.player.directive', ['jamstash.player.service', 'jamstash.settings.service', 'jamstash.subsonic.service', 'jamstash.notifications', 'jamstash.persistence', 'jamstash.page'])
.directive('jplayer', ['$interval', 'player', 'globals', 'subsonic', 'notifications', 'persistence', 'Page',
function($interval, playerService, globals, subsonic, notifications, persistence, Page) {

View file

@ -32,6 +32,7 @@ describe("jplayer directive", function() {
return $delegate;
});
$provide.value('globals', mockGlobals);
$provide.constant('jamstashVersion', '1.0.0');
});
spyOn($.fn, "jPlayer").and.callThrough();

View file

@ -3,7 +3,7 @@
*
* Manages the player and playing queue. Use it to play a song, go to next track or add songs to the queue.
*/
angular.module('jamstash.player.service', ['angular-underscore/utils', 'jamstash.settings'])
angular.module('jamstash.player.service', ['jamstash.settings.service', 'angular-underscore/utils'])
.factory('player', ['globals', function (globals) {
'use strict';

View file

@ -1,9 +1,9 @@
/**
* jamstash.settings Module
* jamstash.settings.service Module
*
* Houses Jamstash's global settings and a few utility functions.
*/
angular.module('jamstash.settings', [])
angular.module('jamstash.settings.service', [])
.service('globals', function () {
'use strict';

View file

@ -1,9 +1,9 @@
describe("settings service", function() {
describe("Settings service", function() {
'use strict';
var globals;
beforeEach(function() {
module('jamstash.settings');
module('jamstash.settings.service');
inject(function (_globals_) {
globals = _globals_;
});

View file

@ -1,7 +1,7 @@
angular.module('JamStash')
angular.module('jamstash.settings.controller', ['jamstash.settings.service', 'jamstash.persistence'])
.controller('SettingsController', ['$rootScope', '$scope', '$routeParams', '$location', 'utils', 'globals', 'json', 'notifications', 'persistence', 'subsonic',
function ($rootScope, $scope, $routeParams, $location, utils, globals, json, notifications, persistence, subsonic) {
.controller('SettingsController', ['$rootScope', '$scope', '$location', 'utils', 'globals', 'json', 'notifications', 'persistence', 'subsonic',
function ($rootScope, $scope, $location, utils, globals, json, notifications, persistence, subsonic) {
'use strict';
$rootScope.hideQueue();
$scope.settings = globals.settings; /* See service.js */
@ -28,8 +28,9 @@
}
});
$scope.reset = function () {
utils.setValue('Settings', null, true);
persistence.deleteSettings();
$scope.loadSettings();
// TODO: Hyz: reload the page
};
$scope.save = function () {
if ($scope.settings.Password !== '' && globals.settings.Password.substring(0, 4) != 'enc:') { $scope.settings.Password = 'enc:' + utils.HexEncode($scope.settings.Password); }
@ -60,7 +61,7 @@
} else {
$rootScope.hideQueue();
}
utils.setValue('Settings', $scope.settings, true);
persistence.saveSettings($scope.settings);
notifications.updateMessage('Settings Updated!', true);
$scope.loadSettings();
if (globals.settings.Server !== '' && globals.settings.Username !== '' && globals.settings.Password !== '') {

View file

@ -0,0 +1,106 @@
describe("Settings controller", function() {
'use strict';
var scope, $rootScope, $controller, $location, $q,
controllerParams, utils, persistence, mockGlobals, json, notifications, subsonic, deferred;
beforeEach(function() {
jasmine.addCustomEqualityTester(angular.equals);
module('jamstash.settings.controller');
mockGlobals = {
settings: {
Password: '',
Server: ''
}
};
// Mock all the services
utils = jasmine.createSpyObj("utils", ["HexEncode"]);
persistence = jasmine.createSpyObj("persistence", ["saveQueue", "deleteQueue", "deleteTrackPosition", "saveSettings", "deleteSettings"]);
json = jasmine.createSpyObj("json", ["getChangeLog"]);
notifications = jasmine.createSpyObj("notifications", ["requestPermissionIfRequired", "isSupported", "updateMessage"]);
inject(function (_$controller_, _$rootScope_, _$location_, _$q_) {
$rootScope = _$rootScope_;
scope = $rootScope.$new();
$location = _$location_;
$q = _$q_;
deferred = $q.defer();
$rootScope.hideQueue = jasmine.createSpy("hideQueue");
$rootScope.loadSettings = jasmine.createSpy("loadSettings");
// Mock subsonic-service using $q
subsonic = jasmine.createSpyObj("subsonic", ["ping"]);
$controller = _$controller_;
controllerParams = {
$rootScope: $rootScope,
$scope: scope,
$location: $location,
utils: utils,
globals: mockGlobals,
json: json,
notifications: notifications,
persistence: persistence,
subsonic: subsonic
};
});
});
describe("", function() {
beforeEach(function() {
$controller('SettingsController', controllerParams);
});
describe("save() -", function() {
it("Given the settings have been set, when I save them, then the settings will be saved using the persistence service and the user will be notified", function() {
scope.save();
expect(persistence.saveSettings).toHaveBeenCalledWith(scope.settings);
expect(notifications.updateMessage).toHaveBeenCalledWith('Settings Updated!', true);
});
it("Given that the SaveTrackPosition setting was true, when I save the settings, then the current queue will be saved using the persistence service", function() {
scope.settings.SaveTrackPosition = true;
scope.save();
expect(persistence.saveQueue).toHaveBeenCalled();
});
it("Given that the SaveTrackPosition setting was false, when I save the settings, then the saved queue and track will be deleted from the persistence service", function() {
scope.settings.SaveTrackPosition = false;
scope.save();
expect(persistence.deleteTrackPosition).toHaveBeenCalled();
expect(persistence.deleteQueue).toHaveBeenCalled();
});
it("Given that the Server, Username and Password settings weren't empty, when I save the settings, then subsonic service will be pinged", function() {
scope.settings.Server = 'http://hexagram.com/malacobdella/liposis?a=platybasic&b=enantiopathia#stratoplane';
scope.settings.Username = 'Mollura';
scope.settings.Password = 'FPTVjZtBwEyq';
subsonic.ping.and.returnValue(deferred.promise);
scope.save();
expect(subsonic.ping).toHaveBeenCalled();
});
});
it("reset() - When I reset the settings, they will be deleted from the persistence service and will be reloaded with default values", function() {
scope.reset();
expect(persistence.deleteSettings).toHaveBeenCalled();
expect(scope.loadSettings).toHaveBeenCalled();
});
});
describe("On startup", function() {
});
});

View file

@ -4,8 +4,8 @@
* Provides access through $http to the Subsonic server's API.
* Also offers more fine-grained functionality that is not part of Subsonic's API.
*/
angular.module('jamstash.subsonic.service', ['jamstash.settings', 'jamstash.utils', 'jamstash.model',
'jamstash.notifications', 'jamstash.player.service', 'angular-underscore/utils'])
angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
'jamstash.settings.service', 'jamstash.utils', 'jamstash.model', 'jamstash.notifications', 'jamstash.player.service'])
.factory('subsonic', ['$rootScope', '$http', '$q', 'globals', 'utils', 'map', 'notifications', 'player',
function ($rootScope, $http, $q, globals, utils, map, notifications, player) {

View file

@ -1,7 +1,8 @@
describe("Subsonic controller", function() {
'use strict';
var scope, $rootScope, $controller, $window, subsonic, notifications, player, controllerParams, deferred;
var scope, $rootScope, $controller, $window,
subsonic, notifications, player, controllerParams, deferred;
beforeEach(function() {
jasmine.addCustomEqualityTester(angular.equals);