Rewrites notification service

- showNotification takes a song as parameter and picks the information it needs itself. That way we avoid player-directive.js having utils as a dependency.
- shortened the names of the methods a bit. Had to rename them in settings.js
- Used $interval rather than window.setInterval in player-directive.js and in notification-service.js. It's easier to unit test them that way !
This commit is contained in:
Hyzual 2015-01-10 17:36:37 +01:00
parent 33f1a88d6b
commit 392ef74eba
5 changed files with 146 additions and 61 deletions

View file

@ -3,12 +3,12 @@
*
* Provides access to the notification UI.
*/
angular.module('jamstash.notifications', ['jamstash.player.service'])
angular.module('jamstash.notifications', ['jamstash.player.service', 'jamstash.utils'])
.service('notifications', ['$rootScope', 'globals', 'player', function($rootScope, globals, player) {
.service('notifications', ['$rootScope', '$window', '$interval', 'globals', 'player', 'utils',
function($rootScope, $window, $interval, globals, player, utils) {
'use strict';
var msgIndex = 1;
this.updateMessage = function (msg, autohide) {
if (msg !== '') {
var id = $rootScope.Messages.push(msg) - 1;
@ -21,49 +21,34 @@ angular.module('jamstash.notifications', ['jamstash.player.service'])
}
};
this.requestPermissionIfRequired = function () {
if (window.Notify.isSupported() && window.Notify.needsPermission()) {
if (this.isSupported() && !this.hasPermission()) {
window.Notify.requestPermission();
}
};
this.hasNotificationPermission = function () {
return (window.Notify.needsPermission() === false);
this.hasPermission = function () {
return !$window.Notify.needsPermission();
};
this.hasNotificationSupport = function () {
this.isSupported = function () {
return window.Notify.isSupported();
};
var notifications = [];
this.showNotification = function (pic, title, text, type, bind) {
if (this.hasNotificationPermission()) {
//closeAllNotifications()
var settings = {};
if (bind === '#NextTrack') {
settings.notifyClick = function () {
this.showNotification = function (song) {
if (this.hasPermission()) {
var notification = new Notify(utils.toHTML.un(song.name), {
body: utils.toHTML.un(song.artist + " - " + song.album),
icon: song.coverartthumb,
notifyClick: function () {
player.nextTrack();
this.close();
//TODO: Hyz: This should be in a directive, so we wouldn't have to use this.
$rootScope.$apply();
};
}
if (type === 'text') {
settings.body = text;
settings.icon = pic;
} else if (type === 'html') {
settings.body = text;
}
var notification = new Notify(title, settings);
notifications.push(notification);
setTimeout(function (notWin) {
notWin.close();
}, globals.settings.Timeout, notification);
});
$interval(function() {
notification.close();
}, globals.settings.Timeout);
notification.show();
} else {
console.log("showNotification: No Permission");
}
};
this.closeAllNotifications = function () {
for (var notification in notifications) {
notifications[notification].close();
}
};
}]);

View file

@ -0,0 +1,108 @@
describe("Notifications service - ", function() {
'use strict';
var notifications, $window, $interval, player, utils, mockGlobals,
NotificationObj;
beforeEach(function() {
mockGlobals = {
settings: {
Timeout: 30000
}
};
module('jamstash.notifications', function ($provide) {
$provide.value('globals', mockGlobals);
});
inject(function (_notifications_, _$window_, _$interval_, _player_, _utils_) {
notifications = _notifications_;
$window = _$window_;
player = _player_;
utils = _utils_;
$interval = _$interval_;
});
spyOn(player, "nextTrack");
spyOn(utils.toHTML, "un").and.callFake(function (arg) { return arg; });
// Mock the Notify object
$window.Notify = jasmine.createSpyObj("Notify", ["isSupported", "needsPermission", "requestPermission"]);
NotificationObj = jasmine.createSpyObj("Notification", ["show", "close"]);
spyOn($window, "Notify").and.callFake(function (title, settings) {
NotificationObj.simulateClick = settings.notifyClick;
return NotificationObj;
});
});
it("can check whether we have the permission to display notifications in the current browser", function() {
$window.Notify.needsPermission.and.returnValue(false);
expect(notifications.hasPermission()).toBeTruthy();
expect($window.Notify.needsPermission).toHaveBeenCalled();
});
it("can check whether the current browser supports notifications", function() {
$window.Notify.isSupported.and.returnValue(true);
expect(notifications.isSupported()).toBeTruthy();
expect($window.Notify.isSupported).toHaveBeenCalled();
});
it("can request Notification permission for the current browser", function() {
spyOn(notifications, "isSupported").and.returnValue(true);
spyOn(notifications, "hasPermission").and.returnValue(false);
notifications.requestPermissionIfRequired();
expect($window.Notify.requestPermission).toHaveBeenCalled();
});
describe("When I show a notification, given a song,", function() {
var song;
beforeEach(function() {
song = {
coverartthumb: "https://backjaw.com/overquantity/outpitch?a=redredge&b=omnivoracious#promotement",
name: "Unhorny",
artist: "Saturnina Koster",
album: "Trepidate"
};
spyOn(notifications, "hasPermission").and.returnValue(true);
});
it("it checks the permissions, displays the title, the artist's name and the album picture in a notification", function() {
notifications.showNotification(song);
expect(notifications.hasPermission).toHaveBeenCalled();
expect($window.Notify).toHaveBeenCalledWith(song.name, {
body: song.artist + " - " + song.album,
icon: song.coverartthumb,
notifyClick: jasmine.any(Function)
});
expect(NotificationObj.show).toHaveBeenCalled();
});
it("when I click on it, it plays the next track of the queue", function() {
notifications.showNotification(song);
NotificationObj.simulateClick();
expect(player.nextTrack).toHaveBeenCalled();
expect(NotificationObj.close).toHaveBeenCalled();
});
it("given that the global Timeout setting is set to 10 seconds, it closes itself after 10 seconds", function() {
mockGlobals.settings.Timeout = 10000;
notifications.showNotification(song);
$interval.flush(10001);
expect(NotificationObj.close).toHaveBeenCalled();
});
it("if we don't have the permission to display notifications, nothing happens", function() {
notifications.hasPermission.and.returnValue(false);
notifications.showNotification(song);
expect(NotificationObj.show).not.toHaveBeenCalled();
});
});
});

View file

@ -4,10 +4,10 @@
* 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.utils', 'jamstash.persistence'])
angular.module('jamstash.player.directive', ['jamstash.player.service', 'jamstash.settings', 'jamstash.subsonic.service', 'jamstash.notifications', 'jamstash.persistence'])
.directive('jplayer', ['player', 'globals', 'subsonic', 'notifications', 'utils', '$window', 'persistence',
function(playerService, globals, subsonic, notifications, utils, $window, persistence) {
.directive('jplayer', ['player', 'globals', 'subsonic', 'notifications', '$interval', 'persistence',
function(playerService, globals, subsonic, notifications, $interval, persistence) {
'use strict';
return {
restrict: 'EA',
@ -100,7 +100,7 @@ angular.module('jamstash.player.directive', ['jamstash.player.service', 'jamstas
} else {
$player.jPlayer('play');
if(globals.settings.NotificationSong) {
notifications.showNotification(newSong.coverartthumb, utils.toHTML.un(newSong.name), utils.toHTML.un(newSong.artist + ' - ' + newSong.album), 'text', '#NextTrack');
notifications.showNotification(newSong);
}
}
}
@ -111,6 +111,7 @@ angular.module('jamstash.player.directive', ['jamstash.player.service', 'jamstas
}, function (newVal) {
if(newVal === true) {
$player.jPlayer('play', 0);
scope.scrobbled = false;
playerService.restartSong = false;
}
});
@ -131,9 +132,9 @@ angular.module('jamstash.player.directive', ['jamstash.player.service', 'jamstas
scope.startSavePosition = function () {
if (globals.settings.SaveTrackPosition) {
if (timerid !== 0) {
$window.clearInterval(timerid);
$interval.cancel(timerid);
}
timerid = $window.setInterval(function () {
timerid = $interval(function () {
var audio = $player.data('jPlayer');
if (globals.settings.SaveTrackPosition && scope.currentSong !== undefined &&
audio !== undefined && audio.status.currentTime > 0 && audio.status.paused === false) {

View file

@ -2,7 +2,7 @@ describe("jplayer directive", function() {
'use strict';
var element, scope, $player, playingSong, deferred,
playerService, mockGlobals, subsonic, notifications, persistence, $window;
playerService, mockGlobals, subsonic, notifications, persistence, $interval;
beforeEach(function() {
// We redefine globals because in some tests we need to alter the settings
@ -24,22 +24,16 @@ describe("jplayer directive", function() {
$delegate.isLastSongPlaying = jasmine.createSpy('isLastSongPlaying');
return $delegate;
});
//TODO: Hyz: We shouldn't have to know the utils service just for that. Remove these calls and deal with this in the Notifications service.
// Mock the utils service
$provide.decorator('utils', function ($delegate) {
$delegate.toHTML.un = jasmine.createSpy('un');
return $delegate;
});
$provide.value('globals', mockGlobals);
});
spyOn($.fn, "jPlayer").and.callThrough();
inject(function($rootScope, $compile, _player_, _subsonic_, _notifications_, _persistence_, _$window_, $q) {
inject(function($rootScope, $compile, _player_, _subsonic_, _notifications_, _persistence_, _$interval_, $q) {
playerService = _player_;
subsonic = _subsonic_;
notifications = _notifications_;
persistence = _persistence_;
$window = _$window_;
$interval = _$interval_;
// Compile the directive
scope = $rootScope.$new();
element = '<div id="playdeck_1" jplayer></div>';
@ -92,18 +86,20 @@ describe("jplayer directive", function() {
scope.$apply();
expect(notifications.showNotification).toHaveBeenCalled();
expect(notifications.showNotification).toHaveBeenCalledWith(playingSong);
});
});
});
it("When the player service's restartSong flag is true, it restarts the current song and resets the flag to false", function() {
it("When the player service's restartSong flag is true, it restarts the current song, resets the restart flag to false and resets the scrobbled flag to false", function() {
$.fn.jPlayer.and.stub();
playerService.restartSong = true;
scope.scrobbled = true;
scope.$apply();
expect($player.jPlayer).toHaveBeenCalledWith('play', 0);
expect(playerService.restartSong).toBeFalsy();
expect(scope.scrobbled).toBeFalsy();
});
describe("When jplayer has finished the current song,", function() {
@ -223,11 +219,6 @@ describe("jplayer directive", function() {
mockGlobals.settings.SaveTrackPosition = true;
spyOn(persistence, "saveTrackPosition");
spyOn(persistence, "saveQueue");
jasmine.clock().install();
});
afterEach(function() {
jasmine.clock().uninstall();
});
it("every 30 seconds, it saves the current song's position and the playing queue", function() {
@ -236,7 +227,7 @@ describe("jplayer directive", function() {
$player.data('jPlayer').status.paused = false;
scope.startSavePosition();
jasmine.clock().tick(30001);
$interval.flush(30001);
expect(scope.currentSong.position).toBe(35.3877);
expect(persistence.saveTrackPosition).toHaveBeenCalledWith(scope.currentSong);
@ -248,18 +239,18 @@ describe("jplayer directive", function() {
$player.data('jPlayer').status.paused = true;
scope.startSavePosition();
jasmine.clock().tick(30001);
$interval.flush(30001);
expect(persistence.saveTrackPosition).not.toHaveBeenCalled();
expect(persistence.saveQueue).not.toHaveBeenCalled();
});
it("if there was already a watcher, it clears it before adding a new one", function() {
spyOn($window, "clearInterval");
spyOn($interval, "cancel");
scope.startSavePosition();
scope.startSavePosition();
expect($window.clearInterval).toHaveBeenCalled();
expect($interval.cancel).toHaveBeenCalled();
});
});
});

View file

@ -36,13 +36,13 @@
if ($scope.settings.Server.indexOf('http://') != 0 && $scope.settings.Server.indexOf('https://') != 0) { $scope.settings.Server = 'http://' + $scope.settings.Server; }
if ($scope.settings.NotificationSong) {
notifications.requestPermissionIfRequired();
if (!notifications.hasNotificationSupport()) {
if (!notifications.isSupported()) {
alert('HTML5 Notifications are not available for your current browser, Sorry :(');
}
}
if ($scope.settings.NotificationNowPlaying) {
notifications.requestPermissionIfRequired();
if (!notifications.hasNotificationSupport()) {
if (!notifications.isSupported()) {
alert('HTML5 Notifications are not available for your current browser, Sorry :(');
}
}