Refactors subsonic's getSongs(), adds a recursiveGetSongs()
So that the separation between subsonic-service and subsonic-controller is clear: the service makes requests and deals with $http, the controller displays, add to the player queue, etc. I've also added a recursive version of getSongs() which enables us to play all the songs in a directory recursively, e.g. all the songs of an artist regardless of their album. - Adds unit-tests to the controller and service for getSongs() and recursiveGetSongs()
This commit is contained in:
parent
7be524af00
commit
ee25f7046e
5 changed files with 326 additions and 97 deletions
|
@ -138,7 +138,7 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
|
||||||
}).then(function (subsonicResponse) {
|
}).then(function (subsonicResponse) {
|
||||||
if(subsonicResponse.indexes !== undefined && (subsonicResponse.indexes.index !== undefined || subsonicResponse.indexes.shortcut !== undefined)) {
|
if(subsonicResponse.indexes !== undefined && (subsonicResponse.indexes.index !== undefined || subsonicResponse.indexes.shortcut !== undefined)) {
|
||||||
// Make sure shortcut, index and each index's artist are arrays
|
// Make sure shortcut, index and each index's artist are arrays
|
||||||
// because Madsonic will return objects and not arrays if there is only 1 artist
|
// because Madsonic will return an object when there's only one element
|
||||||
var formattedResponse = {};
|
var formattedResponse = {};
|
||||||
formattedResponse.shortcut = [].concat(subsonicResponse.indexes.shortcut);
|
formattedResponse.shortcut = [].concat(subsonicResponse.indexes.shortcut);
|
||||||
formattedResponse.index = [].concat(subsonicResponse.indexes.index);
|
formattedResponse.index = [].concat(subsonicResponse.indexes.index);
|
||||||
|
@ -164,6 +164,7 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
|
||||||
params: params
|
params: params
|
||||||
}).then(function (subsonicResponse) {
|
}).then(function (subsonicResponse) {
|
||||||
if(subsonicResponse.directory.child !== undefined) {
|
if(subsonicResponse.directory.child !== undefined) {
|
||||||
|
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
|
||||||
var childArray = [].concat(subsonicResponse.directory.child);
|
var childArray = [].concat(subsonicResponse.directory.child);
|
||||||
if (childArray.length > 0) {
|
if (childArray.length > 0) {
|
||||||
content.song = [];
|
content.song = [];
|
||||||
|
@ -203,6 +204,7 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
|
||||||
params: params
|
params: params
|
||||||
}).then(function (subsonicResponse) {
|
}).then(function (subsonicResponse) {
|
||||||
if(subsonicResponse.albumList.album !== undefined) {
|
if(subsonicResponse.albumList.album !== undefined) {
|
||||||
|
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
|
||||||
var albumArray = [].concat(subsonicResponse.albumList.album);
|
var albumArray = [].concat(subsonicResponse.albumList.album);
|
||||||
if (albumArray.length > 0) {
|
if (albumArray.length > 0) {
|
||||||
content.song = [];
|
content.song = [];
|
||||||
|
@ -253,52 +255,25 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
|
||||||
});
|
});
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
},
|
},
|
||||||
getSongs: function (id, action) {
|
|
||||||
var exception = {reason: 'No songs found on the Subsonic server.'};
|
getSongs: function (id) {
|
||||||
|
var exception = {reason: 'This directory is empty.'};
|
||||||
var promise = subsonicService.subsonicRequest('getMusicDirectory.view', {
|
var promise = subsonicService.subsonicRequest('getMusicDirectory.view', {
|
||||||
params: {
|
params: {
|
||||||
id: id
|
id: id
|
||||||
}
|
}
|
||||||
}).then(function (subsonicResponse) {
|
}).then(function (subsonicResponse) {
|
||||||
if(subsonicResponse.directory.child !== undefined) {
|
if(subsonicResponse.directory.child !== undefined) {
|
||||||
var items = [].concat(subsonicResponse.directory.child);
|
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
|
||||||
if (items.length > 0) {
|
var children = [].concat(subsonicResponse.directory.child);
|
||||||
content.selectedAlbum = id;
|
if (children.length > 0) {
|
||||||
if (action == 'add') {
|
var allChildren = _(children).partition(function (item) {
|
||||||
angular.forEach(items, function (item, key) {
|
return item.isDir;
|
||||||
player.queue.push(map.mapSong(item));
|
});
|
||||||
});
|
return {
|
||||||
notifications.updateMessage(items.length + ' Song(s) Added to Queue', true);
|
directories: map.mapAlbums(allChildren[0]),
|
||||||
} else if (action == 'play') {
|
songs: map.mapSongs(allChildren[1])
|
||||||
player.queue = [];
|
};
|
||||||
angular.forEach(items, function (item, key) {
|
|
||||||
player.queue.push(map.mapSong(item));
|
|
||||||
});
|
|
||||||
var next = player.queue[0];
|
|
||||||
player.play(next);
|
|
||||||
notifications.updateMessage(items.length + ' Song(s) Added to Queue', true);
|
|
||||||
} else {
|
|
||||||
if (subsonicResponse.directory.id != 'undefined') {
|
|
||||||
var albumId = subsonicResponse.directory.id;
|
|
||||||
var albumName = subsonicResponse.directory.name;
|
|
||||||
if (content.breadcrumb.length > 0) { content.breadcrumb.splice(1, (content.breadcrumb.length - 1)); }
|
|
||||||
content.breadcrumb.push({ 'type': 'album', 'id': albumId, 'name': albumName });
|
|
||||||
}
|
|
||||||
content.song = [];
|
|
||||||
content.album = [];
|
|
||||||
var albums = [];
|
|
||||||
angular.forEach(items, function (item, key) {
|
|
||||||
if (item.isDir) {
|
|
||||||
albums.push(map.mapAlbum(item));
|
|
||||||
} else {
|
|
||||||
content.song.push(map.mapSong(item));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (albums.length > 0) {
|
|
||||||
content.album = albums;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return content;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// We end up here for every else
|
// We end up here for every else
|
||||||
|
@ -307,6 +282,41 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
|
||||||
return promise;
|
return promise;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// This is used when we add or play a directory, so we recursively get all its contents
|
||||||
|
recursiveGetSongs: function (id) {
|
||||||
|
var deferred = $q.defer();
|
||||||
|
// We first use getSongs() to get the contents of the root directory
|
||||||
|
subsonicService.getSongs(id).then(function (data) {
|
||||||
|
var directories = data.directories;
|
||||||
|
var songs = data.songs;
|
||||||
|
// If there are only songs, we return them immediately: this is a leaf directory and the end of the recursion
|
||||||
|
if (directories.length === 0) {
|
||||||
|
deferred.resolve(songs);
|
||||||
|
} else {
|
||||||
|
// otherwise, for each directory, we call ourselves
|
||||||
|
var promises = [];
|
||||||
|
angular.forEach(directories, function (dir) {
|
||||||
|
var subdirectoryRequest = subsonicService.recursiveGetSongs(dir.id).then(function (data) {
|
||||||
|
// This is where we join all the songs together in a single array
|
||||||
|
return songs.concat(data);
|
||||||
|
});
|
||||||
|
promises.push(subdirectoryRequest);
|
||||||
|
});
|
||||||
|
// since all of this is asynchronous, we need to wait for all the requests to finish by using $q.all()
|
||||||
|
var allRequestsFinished = $q.all(promises).then(function (data) {
|
||||||
|
// and since $q.all() wraps everything in another array, we use flatten() to end up with only one array of songs
|
||||||
|
return _(data).flatten();
|
||||||
|
});
|
||||||
|
deferred.resolve(allRequestsFinished);
|
||||||
|
}
|
||||||
|
}, function () {
|
||||||
|
// Even if getSongs returns an error, we resolve with an empty array. Otherwise one empty directory somewhere
|
||||||
|
// would keep us from playing all the songs of a directory recursively
|
||||||
|
deferred.resolve([]);
|
||||||
|
});
|
||||||
|
return deferred.promise;
|
||||||
|
},
|
||||||
|
|
||||||
search: function (query, type) {
|
search: function (query, type) {
|
||||||
if(_([0, 1, 2]).contains(type)) {
|
if(_([0, 1, 2]).contains(type)) {
|
||||||
var promise = subsonicService.subsonicRequest('search2.view', {
|
var promise = subsonicService.subsonicRequest('search2.view', {
|
||||||
|
@ -315,6 +325,8 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
|
||||||
}
|
}
|
||||||
}).then(function (subsonicResponse) {
|
}).then(function (subsonicResponse) {
|
||||||
if (!_.isEmpty(subsonicResponse.searchResult2)) {
|
if (!_.isEmpty(subsonicResponse.searchResult2)) {
|
||||||
|
// Make sure that song, album and artist are arrays using concat
|
||||||
|
// because Madsonic will return an object when there's only one element
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 0:
|
case 0:
|
||||||
if (subsonicResponse.searchResult2.song !== undefined) {
|
if (subsonicResponse.searchResult2.song !== undefined) {
|
||||||
|
@ -357,6 +369,7 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
|
||||||
params: params
|
params: params
|
||||||
}).then(function (subsonicResponse) {
|
}).then(function (subsonicResponse) {
|
||||||
if(subsonicResponse.randomSongs !== undefined) {
|
if(subsonicResponse.randomSongs !== undefined) {
|
||||||
|
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
|
||||||
var songArray = [].concat(subsonicResponse.randomSongs.song);
|
var songArray = [].concat(subsonicResponse.randomSongs.song);
|
||||||
if (songArray.length > 0) {
|
if (songArray.length > 0) {
|
||||||
return map.mapSongs(songArray);
|
return map.mapSongs(songArray);
|
||||||
|
@ -384,6 +397,7 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
|
||||||
var promise = subsonicService.getStarred()
|
var promise = subsonicService.getStarred()
|
||||||
.then(function (starred) {
|
.then(function (starred) {
|
||||||
if(starred.song !== undefined) {
|
if(starred.song !== undefined) {
|
||||||
|
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
|
||||||
var songArray = [].concat(starred.song);
|
var songArray = [].concat(starred.song);
|
||||||
if (songArray.length > 0) {
|
if (songArray.length > 0) {
|
||||||
// Return random subarray of songs
|
// Return random subarray of songs
|
||||||
|
@ -402,6 +416,7 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
|
||||||
var promise = subsonicService.subsonicRequest('getPlaylists.view')
|
var promise = subsonicService.subsonicRequest('getPlaylists.view')
|
||||||
.then(function (subsonicResponse) {
|
.then(function (subsonicResponse) {
|
||||||
if(subsonicResponse.playlists.playlist !== undefined) {
|
if(subsonicResponse.playlists.playlist !== undefined) {
|
||||||
|
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
|
||||||
var playlistArray = [].concat(subsonicResponse.playlists.playlist);
|
var playlistArray = [].concat(subsonicResponse.playlists.playlist);
|
||||||
if (playlistArray.length > 0) {
|
if (playlistArray.length > 0) {
|
||||||
var allPlaylists = _(playlistArray).partition(function (item) {
|
var allPlaylists = _(playlistArray).partition(function (item) {
|
||||||
|
@ -424,6 +439,7 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
|
||||||
}
|
}
|
||||||
}).then(function (subsonicResponse) {
|
}).then(function (subsonicResponse) {
|
||||||
if (subsonicResponse.playlist.entry !== undefined) {
|
if (subsonicResponse.playlist.entry !== undefined) {
|
||||||
|
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
|
||||||
var entryArray = [].concat(subsonicResponse.playlist.entry);
|
var entryArray = [].concat(subsonicResponse.playlist.entry);
|
||||||
if (entryArray.length > 0) {
|
if (entryArray.length > 0) {
|
||||||
return map.mapSongs(entryArray);
|
return map.mapSongs(entryArray);
|
||||||
|
@ -514,6 +530,7 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
|
||||||
})
|
})
|
||||||
.then(function (subsonicResponse) {
|
.then(function (subsonicResponse) {
|
||||||
if (subsonicResponse.podcasts !== undefined && subsonicResponse.podcasts.channel !== undefined) {
|
if (subsonicResponse.podcasts !== undefined && subsonicResponse.podcasts.channel !== undefined) {
|
||||||
|
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
|
||||||
var channelArray = [].concat(subsonicResponse.podcasts.channel);
|
var channelArray = [].concat(subsonicResponse.podcasts.channel);
|
||||||
if (channelArray.length > 0) {
|
if (channelArray.length > 0) {
|
||||||
return channelArray;
|
return channelArray;
|
||||||
|
@ -535,10 +552,12 @@ angular.module('jamstash.subsonic.service', ['angular-underscore/utils',
|
||||||
}).then(function (subsonicResponse) {
|
}).then(function (subsonicResponse) {
|
||||||
var episodes = [];
|
var episodes = [];
|
||||||
if (subsonicResponse.podcasts.channel !== undefined) {
|
if (subsonicResponse.podcasts.channel !== undefined) {
|
||||||
|
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
|
||||||
var channelArray = [].concat(subsonicResponse.podcasts.channel);
|
var channelArray = [].concat(subsonicResponse.podcasts.channel);
|
||||||
if (channelArray.length > 0) {
|
if (channelArray.length > 0) {
|
||||||
var channel = channelArray[0];
|
var channel = channelArray[0];
|
||||||
if (channel !== null && channel.id === id) {
|
if (channel !== null && channel.id === id) {
|
||||||
|
// Make sure this is an array using concat because Madsonic will return an object when there's only one element
|
||||||
var episodesArray = [].concat(channel.episode);
|
var episodesArray = [].concat(channel.episode);
|
||||||
episodes = _(episodesArray).filter(function (episode) {
|
episodes = _(episodesArray).filter(function (episode) {
|
||||||
return episode.status === "completed";
|
return episode.status === "completed";
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
describe("Subsonic service -", function() {
|
describe("Subsonic service -", function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var subsonic, mockBackend, mockGlobals,
|
var subsonic, mockBackend, mockGlobals, $q,
|
||||||
response;
|
response, url;
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
// We redefine it because in some tests we need to alter the settings
|
// We redefine it because in some tests we need to alter the settings
|
||||||
mockGlobals = {
|
mockGlobals = {
|
||||||
|
@ -46,9 +46,10 @@ describe("Subsonic service -", function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
inject(function (_subsonic_, $httpBackend) {
|
inject(function (_subsonic_, $httpBackend, _$q_) {
|
||||||
subsonic = _subsonic_;
|
subsonic = _subsonic_;
|
||||||
mockBackend = $httpBackend;
|
mockBackend = $httpBackend;
|
||||||
|
$q = _$q_;
|
||||||
});
|
});
|
||||||
response = {"subsonic-response": {status: "ok", version: "1.10.2"}};
|
response = {"subsonic-response": {status: "ok", version: "1.10.2"}};
|
||||||
});
|
});
|
||||||
|
@ -59,7 +60,7 @@ describe("Subsonic service -", function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("subsonicRequest() -", function() {
|
describe("subsonicRequest() -", function() {
|
||||||
var partialUrl, url;
|
var partialUrl;
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
partialUrl = '/getStarred.view';
|
partialUrl = '/getStarred.view';
|
||||||
url ='http://demo.subsonic.com/rest/getStarred.view?'+
|
url ='http://demo.subsonic.com/rest/getStarred.view?'+
|
||||||
|
@ -129,7 +130,6 @@ describe("Subsonic service -", function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getAlbums() -", function() {
|
describe("getAlbums() -", function() {
|
||||||
var url;
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
url = 'http://demo.subsonic.com/rest/getMusicDirectory.view?'+
|
url = 'http://demo.subsonic.com/rest/getMusicDirectory.view?'+
|
||||||
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&id=21'+'&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
|
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&id=21'+'&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
|
||||||
|
@ -182,62 +182,127 @@ describe("Subsonic service -", function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//TODO: Hyz: Rename into getDirectory(), because we don't know if there will be songs or other directories in it
|
||||||
describe("getSongs() -", function() {
|
describe("getSongs() -", function() {
|
||||||
var url;
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
var url = 'http://demo.subsonic.com/rest/getMusicDirectory.view?'+
|
url = 'http://demo.subsonic.com/rest/getMusicDirectory.view?'+
|
||||||
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&id=209'+'&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
|
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&id=209'+'&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Given that there were 2 songs in the given directory id in my library, when I get the songs from that directory, then a promise will be resolved with an array of 2 songs", function() {
|
it("Given a directory containing 2 songs and 1 subdirectory and given its id, when I get the songs from that directory, then a promise will be resolved with an array of 2 songs and an array of 1 subdirectory", function() {
|
||||||
response["subsonic-response"].directory = {
|
response["subsonic-response"].directory = {
|
||||||
child: [
|
child: [
|
||||||
{ id: 778 },
|
{ id: 778 },
|
||||||
{ id: 614 }
|
{ id: 614 },
|
||||||
|
{ id: 205, isDir: true}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
mockBackend.expectJSONP(url).respond(JSON.stringify(response));
|
mockBackend.expectJSONP(url).respond(JSON.stringify(response));
|
||||||
|
|
||||||
var promise = subsonic.getSongs(209);
|
var promise = subsonic.getSongs(209);
|
||||||
//TODO: Hyz: Replace with toBeResolvedWith() when getSongs() is refactored
|
|
||||||
var success = function (data) {
|
|
||||||
expect(data.album).toEqual([]);
|
|
||||||
expect(data.song).toEqual([
|
|
||||||
{ id: 778 },
|
|
||||||
{ id: 614 }
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
promise.then(success);
|
|
||||||
|
|
||||||
mockBackend.flush();
|
mockBackend.flush();
|
||||||
|
|
||||||
expect(promise).toBeResolved();
|
expect(promise).toBeResolvedWith({
|
||||||
|
directories: [{ id: 205, isDir: true}],
|
||||||
|
songs: [{id: 778}, {id: 614}]
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Given that there was only 1 song in the given directory id in my Madsonic library, when I get the songs from that directory, then a promise will be resolved with an array of 1 song", function() {
|
it("Given a directory containing 1 song in my Madsonic library and given its id, when I get the songs from that directory, then a promise will be resolved with an array of 1 song and an empty array of subdirectories", function() {
|
||||||
response["subsonic-response"].directory = {
|
response["subsonic-response"].directory = {
|
||||||
child: { id: 402 }
|
child: { id: 402 }
|
||||||
};
|
};
|
||||||
mockBackend.expectJSONP(url).respond(JSON.stringify(response));
|
mockBackend.expectJSONP(url).respond(JSON.stringify(response));
|
||||||
|
|
||||||
var promise = subsonic.getSongs(209);
|
var promise = subsonic.getSongs(209);
|
||||||
//TODO: Hyz: Replace with toBeResolvedWith() when getSongs() is refactored
|
|
||||||
var success = function (data) {
|
|
||||||
expect(data.album).toEqual([]);
|
|
||||||
expect(data.song).toEqual([
|
|
||||||
{ id: 402 }
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
promise.then(success);
|
|
||||||
|
|
||||||
mockBackend.flush();
|
mockBackend.flush();
|
||||||
|
|
||||||
expect(promise).toBeResolved();
|
expect(promise).toBeResolvedWith({
|
||||||
|
directories: [],
|
||||||
|
songs: [{id: 402}]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Given a directory that didn't contain anything and given its id, when I get the songs from that directory, then a promise will be rejected with an error message", function() {
|
||||||
|
response["subsonic-response"].directory = {};
|
||||||
|
mockBackend.expectJSONP(url).respond(JSON.stringify(response));
|
||||||
|
|
||||||
|
var promise = subsonic.getSongs(209);
|
||||||
|
mockBackend.flush();
|
||||||
|
|
||||||
|
expect(promise).toBeRejectedWith({reason: 'This directory is empty.'});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("recursiveGetSongs() -", function() {
|
||||||
|
var deferred;
|
||||||
|
beforeEach(function() {
|
||||||
|
deferred = $q.defer();
|
||||||
|
spyOn(subsonic, 'getSongs').and.returnValue(deferred.promise);
|
||||||
|
});
|
||||||
|
it("Given a directory containing 2 songs and a subdirectory itself containing 2 songs and given its id, when I get the songs from that directory, then a promise will be resolved with an array of 4 songs", function() {
|
||||||
|
// Mock getSongs so we are only testing the recursivity
|
||||||
|
var firstDeferred = $q.defer();
|
||||||
|
var secondDeferred = $q.defer();
|
||||||
|
subsonic.getSongs.and.callFake(function (id) {
|
||||||
|
// First call to getSongs
|
||||||
|
if (id === 499) {
|
||||||
|
return firstDeferred.promise;
|
||||||
|
// Second call to getSongs
|
||||||
|
} else if (id === 553) {
|
||||||
|
return secondDeferred.promise;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var promise = subsonic.recursiveGetSongs(499);
|
||||||
|
// On the first call to getSongs, we expect 2 songs and a subdirectory
|
||||||
|
firstDeferred.resolve({
|
||||||
|
directories: [{ id: 553, type: 'byfolder' }],
|
||||||
|
songs: [
|
||||||
|
{ id: 695 },
|
||||||
|
{ id: 227 }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
// On the second call, we expect 2 songs
|
||||||
|
secondDeferred.resolve({
|
||||||
|
directories: [],
|
||||||
|
songs: [
|
||||||
|
{ id: 422 },
|
||||||
|
{ id: 171 }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(promise).toBeResolvedWith([
|
||||||
|
{ id: 695 },
|
||||||
|
{ id: 227 },
|
||||||
|
{ id: 422 },
|
||||||
|
{ id: 171 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Given a directory containing only 2 songs and given its id, when I get the songs from that directory, then a promise will be resolved with an array of 2 songs", function() {
|
||||||
|
var promise = subsonic.recursiveGetSongs(14);
|
||||||
|
deferred.resolve({
|
||||||
|
directories: [],
|
||||||
|
songs: [
|
||||||
|
{ id: 33 }, { id: 595 }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(promise).toBeResolvedWith([
|
||||||
|
{ id: 33 }, { id: 595 }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Given a directory that didn't contain anything and given its id, when I get the songs from that directory, then a promise will be resolved with an empty array", function() {
|
||||||
|
var promise = subsonic.recursiveGetSongs(710);
|
||||||
|
deferred.reject({reason: 'This directory is empty.'});
|
||||||
|
|
||||||
|
expect(promise).toBeResolvedWith([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getAlbumListBy() -", function() {
|
describe("getAlbumListBy() -", function() {
|
||||||
var url;
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
url = 'http://demo.subsonic.com/rest/getAlbumList.view?'+
|
url = 'http://demo.subsonic.com/rest/getAlbumList.view?'+
|
||||||
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&offset=0'+'&p=enc:cGFzc3dvcmQ%3D'+'&size=3&type=newest'+'&u=Hyzual&v=1.10.2';
|
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&offset=0'+'&p=enc:cGFzc3dvcmQ%3D'+'&size=3&type=newest'+'&u=Hyzual&v=1.10.2';
|
||||||
|
@ -331,7 +396,6 @@ describe("Subsonic service -", function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getRandomStarredSongs() -", function() {
|
describe("getRandomStarredSongs() -", function() {
|
||||||
var url;
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
url = 'http://demo.subsonic.com/rest/getStarred.view?'+
|
url = 'http://demo.subsonic.com/rest/getStarred.view?'+
|
||||||
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
|
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
|
||||||
|
@ -393,7 +457,6 @@ describe("Subsonic service -", function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getRandomSongs() -", function() {
|
describe("getRandomSongs() -", function() {
|
||||||
var url;
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
url = 'http://demo.subsonic.com/rest/getRandomSongs.view?'+
|
url = 'http://demo.subsonic.com/rest/getRandomSongs.view?'+
|
||||||
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&p=enc:cGFzc3dvcmQ%3D'+'&size=3'+'&u=Hyzual&v=1.10.2';
|
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&p=enc:cGFzc3dvcmQ%3D'+'&size=3'+'&u=Hyzual&v=1.10.2';
|
||||||
|
@ -508,7 +571,6 @@ describe("Subsonic service -", function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getArtists() -", function() {
|
describe("getArtists() -", function() {
|
||||||
var url;
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
url = 'http://demo.subsonic.com/rest/getIndexes.view?'+
|
url = 'http://demo.subsonic.com/rest/getIndexes.view?'+
|
||||||
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
|
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
|
||||||
|
@ -689,7 +751,6 @@ describe("Subsonic service -", function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getPlaylist() -", function() {
|
describe("getPlaylist() -", function() {
|
||||||
var url;
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
url = 'http://demo.subsonic.com/rest/getPlaylist.view?'+
|
url = 'http://demo.subsonic.com/rest/getPlaylist.view?'+
|
||||||
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&id=9123'+'&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
|
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&id=9123'+'&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
|
||||||
|
@ -781,7 +842,6 @@ describe("Subsonic service -", function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getPodcasts() -", function() {
|
describe("getPodcasts() -", function() {
|
||||||
var url;
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
url = 'http://demo.subsonic.com/rest/getPodcasts.view?'+
|
url = 'http://demo.subsonic.com/rest/getPodcasts.view?'+
|
||||||
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&includeEpisodes=false'+'&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
|
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&includeEpisodes=false'+'&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
|
||||||
|
@ -833,7 +893,6 @@ describe("Subsonic service -", function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getPodcast() -", function() {
|
describe("getPodcast() -", function() {
|
||||||
var url;
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
url = 'http://demo.subsonic.com/rest/getPodcasts.view?'+
|
url = 'http://demo.subsonic.com/rest/getPodcasts.view?'+
|
||||||
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&id=2695&includeEpisodes=true'+'&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
|
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp'+'&id=2695&includeEpisodes=true'+'&p=enc:cGFzc3dvcmQ%3D&u=Hyzual&v=1.10.2';
|
||||||
|
@ -933,7 +992,6 @@ describe("Subsonic service -", function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("search() -", function() {
|
describe("search() -", function() {
|
||||||
var url;
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
url = 'http://demo.subsonic.com/rest/search2.view?'+
|
url = 'http://demo.subsonic.com/rest/search2.view?'+
|
||||||
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&p=enc:cGFzc3dvcmQ%3D'+'&query=unintersetingly'+'&u=Hyzual&v=1.10.2';
|
'c=Jamstash&callback=JSON_CALLBACK&f=jsonp&p=enc:cGFzc3dvcmQ%3D'+'&query=unintersetingly'+'&u=Hyzual&v=1.10.2';
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
<div id="BreadCrumbs" class="floatleft">
|
<div id="BreadCrumbs" class="floatleft">
|
||||||
<div class="crumb"><a ng-click="toggleArtists()" title="Toggle Artists">Artists</a> ></div>
|
<div class="crumb"><a ng-click="toggleArtists()" title="Toggle Artists">Artists</a> ></div>
|
||||||
<div class="crumb" ng-repeat="o in BreadCrumbs | filter:{type:'artist'}"><a ng-click="getAlbums(o.id, o.name)">{{o.name}}</a> ></div>
|
<div class="crumb" ng-repeat="o in BreadCrumbs | filter:{type:'artist'}"><a ng-click="getAlbums(o.id, o.name)">{{o.name}}</a> ></div>
|
||||||
<div class="crumb" ng-repeat="o in BreadCrumbs | filter:{type:'album'}"><a ng-click="getSongs(o.id, '')">{{o.name}}</a> ></div>
|
<div class="crumb" ng-repeat="o in BreadCrumbs | filter:{type:'album'}"><a ng-click="getSongs('display', o.id, o.name)">{{o.name}}</a> ></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
@ -49,10 +49,10 @@
|
||||||
<div class="clear"></div>
|
<div class="clear"></div>
|
||||||
<ul class="simplelist songlist noselect">
|
<ul class="simplelist songlist noselect">
|
||||||
<div class="" ng-repeat="o in album" ng-switch on="o.type">
|
<div class="" ng-repeat="o in album" ng-switch on="o.type">
|
||||||
<li class="album" ng-switch-when="byfolder" id="{{o.id}}" ng-class="{'selected': selectedAlbum == o.id, 'albumgrid': settings.DefaultLibraryLayout.id == 'grid'}" ng-click="getSongs(o.id, '')" parentid="{{o.parentid}}">
|
<li class="album" ng-switch-when="byfolder" id="{{o.id}}" ng-class="{'selected': selectedAlbum == o.id, 'albumgrid': settings.DefaultLibraryLayout.id == 'grid'}" ng-click="getSongs('display', o.id, o.name)" parentid="{{o.parentid}}">
|
||||||
<div class="itemactions">
|
<div class="itemactions">
|
||||||
<a class="add hover" href="" title="Add To Play Queue" ng-click="getSongs(o.id, 'add')" stop-event="click"></a>
|
<a class="add hover" href="" title="Add To Play Queue" ng-click="getSongs('add', o.id, o.name)" stop-event="click"></a>
|
||||||
<a class="play hover" href="" title="Play" ng-click="getSongs(o.id, 'play')" stop-event="click"></a>
|
<a class="play hover" href="" title="Play" ng-click="getSongs('play', o.id, o.name)" stop-event="click"></a>
|
||||||
<a class="download hover" href="" ng-click="download(o.id)" title="Download" stop-event="click"></a>
|
<a class="download hover" href="" ng-click="download(o.id)" title="Download" stop-event="click"></a>
|
||||||
<a title="Favorite hover" href="" ng-class="{'favorite': o.starred, 'rate': !o.starred}" ng-click="updateFavorite(o)" stop-event="click"></a>
|
<a title="Favorite hover" href="" ng-class="{'favorite': o.starred, 'rate': !o.starred}" ng-click="updateFavorite(o)" stop-event="click"></a>
|
||||||
<a class="info hover" href="" title="{{'Created: ' + o.date}}"></a>
|
<a class="info hover" href="" title="{{'Created: ' + o.date}}"></a>
|
||||||
|
|
|
@ -279,10 +279,11 @@ angular.module('jamstash.subsonic.controller', ['jamstash.subsonic.service', 'ja
|
||||||
* @param {String} action the action to be taken with the songs. Must be 'add', 'play' or 'display'
|
* @param {String} action the action to be taken with the songs. Must be 'add', 'play' or 'display'
|
||||||
* @return {Promise} the original promise passed in first param. That way we can chain it further !
|
* @return {Promise} the original promise passed in first param. That way we can chain it further !
|
||||||
*/
|
*/
|
||||||
|
//TODO: Hyz: Maybe we should move this to a service
|
||||||
$scope.requestSongs = function (promise, action) {
|
$scope.requestSongs = function (promise, action) {
|
||||||
$scope.handleErrors(promise)
|
$scope.handleErrors(promise)
|
||||||
.then(function (songs) {
|
.then(function (songs) {
|
||||||
if(action === 'play') {
|
if (action === 'play') {
|
||||||
player.emptyQueue().addSongs(songs).playFirstSong();
|
player.emptyQueue().addSongs(songs).playFirstSong();
|
||||||
notifications.updateMessage(songs.length + ' Song(s) Added to Queue', true);
|
notifications.updateMessage(songs.length + ' Song(s) Added to Queue', true);
|
||||||
} else if (action === 'add') {
|
} else if (action === 'add') {
|
||||||
|
@ -300,18 +301,33 @@ angular.module('jamstash.subsonic.controller', ['jamstash.subsonic.service', 'ja
|
||||||
return promise;
|
return promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.getSongs = function (id, action) {
|
$scope.getSongs = function (action, id, name) {
|
||||||
subsonic.getSongs(id, action).then(function (data) {
|
var promise;
|
||||||
$scope.album = data.album;
|
if (action === 'play' || action === 'add') {
|
||||||
$scope.song = data.song;
|
promise = subsonic.recursiveGetSongs(id);
|
||||||
$scope.BreadCrumbs = data.breadcrumb;
|
$scope.requestSongs(promise, action);
|
||||||
$scope.selectedAutoAlbum = data.selectedAutoAlbum;
|
} else if (action === 'display') {
|
||||||
$scope.selectedArtist = data.selectedArtist;
|
promise = subsonic.getSongs(id);
|
||||||
$scope.selectedPlaylist = data.selectedPlaylist;
|
$scope.handleErrors(promise).then(function (data) {
|
||||||
if ($scope.SelectedAlbumSort.id != "default") {
|
$scope.album = data.directories;
|
||||||
sortSubsonicAlbums($scope.SelectedAlbumSort.id);
|
$scope.song = data.songs;
|
||||||
}
|
if ($scope.BreadCrumbs.length > 0) {
|
||||||
});
|
$scope.BreadCrumbs.splice(1, ($scope.BreadCrumbs.length - 1));
|
||||||
|
}
|
||||||
|
$scope.BreadCrumbs.push({'type': 'album', 'id': id, 'name': name});
|
||||||
|
$scope.selectedAutoAlbum = null;
|
||||||
|
$scope.selectedArtist = null;
|
||||||
|
$scope.selectedAlbum = id;
|
||||||
|
$scope.selectedAutoPlaylist = null;
|
||||||
|
$scope.selectedPlaylist = null;
|
||||||
|
$scope.selectedPodcast = null;
|
||||||
|
if ($scope.SelectedAlbumSort.id !== "default") {
|
||||||
|
sortSubsonicAlbums($scope.SelectedAlbumSort.id);
|
||||||
|
}
|
||||||
|
}, function (error) {
|
||||||
|
notifications.updateMessage(error.reason, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.getRandomStarredSongs = function (action) {
|
$scope.getRandomStarredSongs = function (action) {
|
||||||
|
|
|
@ -22,6 +22,8 @@ describe("Subsonic controller", function() {
|
||||||
|
|
||||||
// Mock the subsonic service
|
// Mock the subsonic service
|
||||||
subsonic = jasmine.createSpyObj("subsonic", [
|
subsonic = jasmine.createSpyObj("subsonic", [
|
||||||
|
"getSongs",
|
||||||
|
"recursiveGetSongs",
|
||||||
"getAlbums",
|
"getAlbums",
|
||||||
"getArtists",
|
"getArtists",
|
||||||
"getGenres",
|
"getGenres",
|
||||||
|
@ -38,6 +40,8 @@ describe("Subsonic controller", function() {
|
||||||
]);
|
]);
|
||||||
// We make them return different promises and use our deferred variable only when testing
|
// We make them return different promises and use our deferred variable only when testing
|
||||||
// a particular function, so that they stay isolated
|
// a particular function, so that they stay isolated
|
||||||
|
subsonic.getSongs.and.returnValue($q.defer().promise);
|
||||||
|
subsonic.recursiveGetSongs.and.returnValue($q.defer().promise);
|
||||||
subsonic.getAlbums.and.returnValue($q.defer().promise);
|
subsonic.getAlbums.and.returnValue($q.defer().promise);
|
||||||
subsonic.getArtists.and.returnValue($q.defer().promise);
|
subsonic.getArtists.and.returnValue($q.defer().promise);
|
||||||
subsonic.getGenres.and.returnValue($q.defer().promise);
|
subsonic.getGenres.and.returnValue($q.defer().promise);
|
||||||
|
@ -80,6 +84,138 @@ describe("Subsonic controller", function() {
|
||||||
scope.selectedPlaylist = null;
|
scope.selectedPlaylist = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getSongs -", function() {
|
||||||
|
beforeEach(function() {
|
||||||
|
scope.BreadCrumbs = [];
|
||||||
|
scope.SelectedAlbumSort= {
|
||||||
|
id: "default"
|
||||||
|
};
|
||||||
|
subsonic.getSongs.and.returnValue(deferred.promise);
|
||||||
|
subsonic.recursiveGetSongs.and.returnValue(deferred.promise);
|
||||||
|
spyOn(scope, "requestSongs").and.returnValue(deferred.promise);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Given a music directory that contained 3 songs and given its id and name, when I display it, then subsonic-service will be called, the breadcrumbs will be updated and the songs will be published to the scope", function() {
|
||||||
|
scope.getSongs('display', 87, 'Covetous Dadayag');
|
||||||
|
deferred.resolve({
|
||||||
|
directories: [],
|
||||||
|
songs: [
|
||||||
|
{ id: 660 },
|
||||||
|
{ id: 859 },
|
||||||
|
{ id: 545 }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
scope.$apply();
|
||||||
|
|
||||||
|
expect(subsonic.getSongs).toHaveBeenCalledWith(87);
|
||||||
|
expect(scope.album).toEqual([]);
|
||||||
|
expect(scope.song).toEqual([
|
||||||
|
{ id: 660 },
|
||||||
|
{ id: 859 },
|
||||||
|
{ id: 545 }
|
||||||
|
]);
|
||||||
|
expect(scope.BreadCrumbs).toEqual([{
|
||||||
|
type: 'album',
|
||||||
|
id: 87,
|
||||||
|
name: 'Covetous Dadayag'
|
||||||
|
}]);
|
||||||
|
expect(scope.selectedAutoAlbum).toBeNull();
|
||||||
|
expect(scope.selectedArtist).toBeNull();
|
||||||
|
expect(scope.selectedAlbum).toBe(87);
|
||||||
|
expect(scope.selectedAutoPlaylist).toBeNull();
|
||||||
|
expect(scope.selectedPlaylist).toBeNull();
|
||||||
|
expect(scope.selectedPodcast).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Given that there was a previous level in the breadcrumbs, when I display a music directory, then the album breadcrumb will be added", function() {
|
||||||
|
scope.BreadCrumbs = [
|
||||||
|
{
|
||||||
|
type: 'artist',
|
||||||
|
id: 73,
|
||||||
|
name: 'Evan Mestanza'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
scope.getSongs('display', 883, 'Pitiedly preutilizable');
|
||||||
|
deferred.resolve({
|
||||||
|
directories: [],
|
||||||
|
songs: []
|
||||||
|
});
|
||||||
|
scope.$apply();
|
||||||
|
|
||||||
|
expect(scope.BreadCrumbs).toEqual([
|
||||||
|
{
|
||||||
|
type: 'artist',
|
||||||
|
id: 73,
|
||||||
|
name: 'Evan Mestanza'
|
||||||
|
}, {
|
||||||
|
type: 'album',
|
||||||
|
id: 883,
|
||||||
|
name: 'Pitiedly preutilizable'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Given a music directory that contained 2 songs and 1 subdirectory and given its id and name, when I display it, then subsonic-service will be called, the songs and directory will be published to the scope", function() {
|
||||||
|
scope.getSongs('display', 6, 'Potsander dormilona');
|
||||||
|
deferred.resolve({
|
||||||
|
directories: [{id: 387, type: 'byfolder'}],
|
||||||
|
songs: [
|
||||||
|
{ id: 102 },
|
||||||
|
{ id: 340 }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
scope.$apply();
|
||||||
|
|
||||||
|
expect(scope.album).toEqual([
|
||||||
|
{id: 387, type: 'byfolder'}
|
||||||
|
]);
|
||||||
|
expect(scope.song).toEqual([
|
||||||
|
{ id: 102 },
|
||||||
|
{ id: 340 }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Given a music directory, when I display it, then handleErrors will handle HTTP and Subsonic errors", function() {
|
||||||
|
spyOn(scope, 'handleErrors').and.returnValue(deferred.promise);
|
||||||
|
scope.getSongs('display', 88);
|
||||||
|
expect(scope.handleErrors).toHaveBeenCalledWith(deferred.promise);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Given a music directory that didn't contain anything, when I display it, then an error notification will be displayed", function() {
|
||||||
|
scope.getSongs('display', 214, 'Upside bunemost');
|
||||||
|
deferred.reject({reason: 'This directory is empty.'});
|
||||||
|
scope.$apply();
|
||||||
|
|
||||||
|
expect(notifications.updateMessage).toHaveBeenCalledWith('This directory is empty.', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Given a music directory that contained 3 songs and given its id, when I add it to the playing queue, then requestSongs() will be called", function() {
|
||||||
|
scope.getSongs('add', 720);
|
||||||
|
deferred.resolve([
|
||||||
|
{ id: 927 },
|
||||||
|
{ id: 598 },
|
||||||
|
{ id: 632 }
|
||||||
|
]);
|
||||||
|
scope.$apply();
|
||||||
|
|
||||||
|
expect(subsonic.recursiveGetSongs).toHaveBeenCalledWith(720);
|
||||||
|
expect(scope.requestSongs).toHaveBeenCalledWith(deferred.promise, 'add');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Given a music directory that contained 3 songs and given its id, when I play it, then requestSongs() will be called", function() {
|
||||||
|
scope.getSongs('play', 542);
|
||||||
|
deferred.resolve([
|
||||||
|
{ id: 905 },
|
||||||
|
{ id: 181 },
|
||||||
|
{ id: 880 }
|
||||||
|
]);
|
||||||
|
scope.$apply();
|
||||||
|
|
||||||
|
expect(subsonic.recursiveGetSongs).toHaveBeenCalledWith(542);
|
||||||
|
expect(scope.requestSongs).toHaveBeenCalledWith(deferred.promise, 'play');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("Given that my library contained 3 songs, ", function() {
|
describe("Given that my library contained 3 songs, ", function() {
|
||||||
var response;
|
var response;
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue