diff --git a/test/spec/controllers/inventoryCtrlSpec.js b/test/spec/controllers/inventoryCtrlSpec.js index c3bae813f7..955b6f4f0c 100644 --- a/test/spec/controllers/inventoryCtrlSpec.js +++ b/test/spec/controllers/inventoryCtrlSpec.js @@ -1,7 +1,7 @@ 'use strict'; describe('Inventory Controller', function() { - var scope, ctrl, user, rootScope; + var scope, ctrl, user, rootScope, shared, achievement; beforeEach(function() { module(function($provide) { @@ -15,7 +15,7 @@ describe('Inventory Controller', function() { $provide.value('$window', mockWindow); }); - inject(function($rootScope, $controller, Shared, User, $location, $window) { + inject(function($rootScope, $controller, Shared, User, $location, $window, Achievement) { user = specHelper.newUser({ balance: 4, items: { @@ -32,6 +32,8 @@ describe('Inventory Controller', function() { }); Shared.wrap(user); + shared = Shared; + achievement = Achievement; scope = $rootScope.$new(); rootScope = $rootScope; @@ -118,6 +120,27 @@ describe('Inventory Controller', function() { expect(rootScope.openModal).to.not.be.called; }); + + it('shows beastMaster achievement modal if user has all 90 pets', function(){ + sandbox.stub(achievement, 'displayAchievement'); + sandbox.stub(shared.count, "beastMasterProgress").returns(90); + scope.chooseEgg('Cactus'); + scope.choosePotion('Base'); + + expect(achievement.displayAchievement).to.be.called; + expect(achievement.displayAchievement).to.be.calledWith('beastMaster'); + }); + + it('shows triadBingo achievement modal if user has all pets twice and all mounts', function(){ + sandbox.stub(achievement, 'displayAchievement'); + sandbox.stub(shared.count, "mountMasterProgress").returns(90); + sandbox.stub(shared.count, "dropPetsCurrentlyOwned").returns(90); + scope.chooseEgg('Cactus'); + scope.choosePotion('Base'); + + expect(achievement.displayAchievement).to.be.called; + expect(achievement.displayAchievement).to.be.calledWith('triadBingo'); + }); }); describe('Feeding and Raising Pets', function() { @@ -194,6 +217,16 @@ describe('Inventory Controller', function() { expect(rootScope.openModal).to.have.been.calledOnce; expect(rootScope.openModal).to.have.been.calledWith('raisePet'); }); + + it('shows mountMaster achievement modal if user has all 90 mounts', function(){ + sandbox.stub(achievement, 'displayAchievement'); + sandbox.stub(shared.count, "mountMasterProgress").returns(90); + scope.chooseFood('Meat'); + scope.choosePet('PandaCub','Base'); + + expect(achievement.displayAchievement).to.be.calledOnce; + expect(achievement.displayAchievement).to.be.calledWith('mountMaster'); + }); }); it('sells an egg', function(){ diff --git a/test/spec/controllers/notificationCtrlSpec.js b/test/spec/controllers/notificationCtrlSpec.js index 5687eea182..bcd2072b12 100644 --- a/test/spec/controllers/notificationCtrlSpec.js +++ b/test/spec/controllers/notificationCtrlSpec.js @@ -1,33 +1,49 @@ 'use strict'; describe('Notification Controller', function() { - var user, scope, rootScope, ctrl; + var user, scope, rootScope, fakeBackend, achievement, ctrl; beforeEach(function() { user = specHelper.newUser(); user._id = "unique-user-id"; + var userSync = sinon.stub().returns({ + then: function then (f) { f(); } + }); + + let User = { + user, + readNotification: function noop () {}, + sync: userSync + }; + module(function($provide) { - $provide.value('User', {user: user}); + $provide.value('User', User); $provide.value('Guide', {}); }); - inject(function(_$rootScope_, _$controller_) { + inject(function(_$rootScope_, $httpBackend, _$controller_, Achievement, Shared) { scope = _$rootScope_.$new(); rootScope = _$rootScope_; - // Load RootCtrl to ensure shared behaviors are loaded - _$controller_('RootCtrl', {$scope: scope, User: {user: user}}); + fakeBackend = $httpBackend; + fakeBackend.when('GET', 'partials/main.html').respond({}); - ctrl = _$controller_('NotificationCtrl', {$scope: scope, User: {user: user}}); + achievement = Achievement; + + Shared.wrap(user); + + // Load RootCtrl to ensure shared behaviors are loaded + _$controller_('RootCtrl', {$scope: scope, User}); + + ctrl = _$controller_('NotificationCtrl', {$scope: scope, User}); }); + + sandbox.stub(rootScope, 'openModal'); + sandbox.stub(achievement, 'displayAchievement'); }); describe('Quest Invitation modal watch', function() { - beforeEach(function() { - sandbox.stub(rootScope, 'openModal'); - }); - it('opens quest invitation modal', function() { user.party.quest.RSVPNeeded = true; delete user.party.quest.completed; @@ -55,10 +71,6 @@ describe('Notification Controller', function() { }); describe('Quest Completion modal watch', function() { - beforeEach(function() { - sandbox.stub(rootScope, 'openModal'); - }); - it('opens quest completion modal', function() { user.party.quest.completed = "hedgebeast"; scope.$digest(); @@ -84,4 +96,94 @@ describe('Notification Controller', function() { expect(rootScope.openModal).to.not.be.called; }); }); + + describe('User challenge won notification watch', function() { + it('opens challenge won modal when a challenge-won notification is recieved', function() { + rootScope.$digest(); + rootScope.userNotifications.push({type: 'WON_CHALLENGE'}); + rootScope.$digest(); + + expect(achievement.displayAchievement).to.be.called; + expect(achievement.displayAchievement).to.be.calledWith('wonChallenge'); + }); + + it('does not open challenge won modal if no new challenge-won notification is recieved', function() { + rootScope.$digest(); + rootScope.$digest(); + + expect(achievement.displayAchievement).to.not.be.calledWith('wonChallenge'); + }); + }); + + describe('User streak achievement notification watch', function() { + it('opens streak achievement modal when a streak-achievement notification is recieved', function() { + rootScope.$digest(); + rootScope.userNotifications.push({type: 'STREAK_ACHIEVEMENT'}); + rootScope.$digest(); + + expect(achievement.displayAchievement).to.be.called; + expect(achievement.displayAchievement).to.be.calledWith('streak', {size: 'md'}); + }); + + it('does not open streak achievement modal if no new streak-achievement notification is recieved', function() { + rootScope.$digest(); + rootScope.$digest(); + + expect(achievement.displayAchievement).to.not.be.calledWith('streak', {size: 'md'}); + }); + }); + + describe('User ultimate gear set achievement notification watch', function() { + it('opens ultimate gear set achievement modal when an ultimate-gear-achievement notification is recieved', function() { + rootScope.$digest(); + rootScope.userNotifications.push({type: 'ULTIMATE_GEAR_ACHIEVEMENT'}); + rootScope.$digest(); + + expect(achievement.displayAchievement).to.be.called; + expect(achievement.displayAchievement).to.be.calledWith('ultimateGear', {size: 'md'}); + }); + + it('does not open ultimate gear set achievement modal if no new ultimate-gear-achievement notification is recieved', function() { + rootScope.$digest(); + rootScope.$digest(); + + expect(achievement.displayAchievement).to.not.be.calledWith('ultimateGear', {size: 'md'}); + }); + }); + + describe('User rebirth achievement notification watch', function() { + it('opens rebirth achievement modal when a rebirth-achievement notification is recieved', function() { + rootScope.$digest(); + rootScope.userNotifications.push({type: 'REBIRTH_ACHIEVEMENT'}); + rootScope.$digest(); + + expect(achievement.displayAchievement).to.be.called; + expect(achievement.displayAchievement).to.be.calledWith('rebirth'); + }); + + it('does not open rebirth achievement modal if no new rebirth-achievement notification is recieved', function() { + rootScope.$digest(); + rootScope.$digest(); + + expect(achievement.displayAchievement).to.not.be.calledWith('rebirth'); + }); + }); + + describe('User contributor achievement notification watch', function() { + it('opens contributor achievement modal when a new-contributor-level notification is recieved', function() { + rootScope.$digest(); + rootScope.userNotifications.push({type: 'NEW_CONTRIBUTOR_LEVEL'}); + rootScope.$digest(); + + expect(achievement.displayAchievement).to.be.called; + expect(achievement.displayAchievement).to.be.calledWith('contributor', {size: 'md'}); + }); + + it('does not open contributor achievement modal if no new new-contributor-level notification is recieved', function() { + rootScope.$digest(); + rootScope.$digest(); + + expect(achievement.displayAchievement).to.not.be.calledWith('contributor', {size: 'md'}); + }); + }); }); diff --git a/test/spec/controllers/partyCtrlSpec.js b/test/spec/controllers/partyCtrlSpec.js index 433bd6d0fe..e86919dc51 100644 --- a/test/spec/controllers/partyCtrlSpec.js +++ b/test/spec/controllers/partyCtrlSpec.js @@ -1,8 +1,7 @@ 'use strict'; describe("Party Controller", function() { - var scope, ctrl, user, User, questsService, groups, rootScope, $controller, deferred; - var party; + var scope, ctrl, user, User, questsService, groups, achievement, rootScope, $controller, deferred, party; beforeEach(function() { user = specHelper.newUser(), @@ -23,7 +22,7 @@ describe("Party Controller", function() { $provide.value('User', User); }); - inject(function(_$rootScope_, _$controller_, Groups, Quests, _$q_){ + inject(function(_$rootScope_, _$controller_, Groups, Quests, _$q_, Achievement){ rootScope = _$rootScope_; @@ -33,6 +32,7 @@ describe("Party Controller", function() { groups = Groups; questsService = Quests; + achievement = Achievement; // Load RootCtrl to ensure shared behaviors are loaded $controller('RootCtrl', {$scope: scope, User: User}); @@ -61,7 +61,7 @@ describe("Party Controller", function() { }; beforeEach(function() { - sandbox.stub(rootScope, 'openModal'); + sandbox.stub(achievement, 'displayAchievement'); }); context('party has 1 member', function() { @@ -71,7 +71,7 @@ describe("Party Controller", function() { initializeControllerWithStubbedState(); expect(User.set).to.not.be.called; - expect(rootScope.openModal).to.not.be.called; + expect(achievement.displayAchievement).to.not.be.called; }); }); @@ -87,8 +87,8 @@ describe("Party Controller", function() { expect(User.set).to.be.calledWith( { 'achievements.partyUp': true } ); - expect(rootScope.openModal).to.be.calledOnce; - expect(rootScope.openModal).to.be.calledWith('achievements/partyUp'); + expect(achievement.displayAchievement).to.be.calledOnce; + expect(achievement.displayAchievement).to.be.calledWith('partyUp'); done(); }, 1000); }); @@ -112,8 +112,8 @@ describe("Party Controller", function() { expect(User.set).to.be.calledWith( { 'achievements.partyOn': true } ); - expect(rootScope.openModal).to.be.calledOnce; - expect(rootScope.openModal).to.be.calledWith('achievements/partyOn'); + expect(achievement.displayAchievement).to.be.calledOnce; + expect(achievement.displayAchievement).to.be.calledWith('partyOn'); done(); }, 1000); }); @@ -131,9 +131,9 @@ describe("Party Controller", function() { expect(User.set).to.be.calledWith( { 'achievements.partyOn': true} ); - expect(rootScope.openModal).to.have.been.called; - expect(rootScope.openModal).to.be.calledWith('achievements/partyUp'); - expect(rootScope.openModal).to.be.calledWith('achievements/partyOn'); + expect(achievement.displayAchievement).to.have.been.called; + expect(achievement.displayAchievement).to.be.calledWith('partyUp'); + expect(achievement.displayAchievement).to.be.calledWith('partyOn'); done(); }, 1000); }); @@ -147,7 +147,7 @@ describe("Party Controller", function() { initializeControllerWithStubbedState(); expect(User.set).to.not.be.called; - expect(rootScope.openModal).to.not.be.called; + expect(achievement.displayAchievement).to.not.be.called; }); }); }); diff --git a/test/spec/services/achievementServicesSpec.js b/test/spec/services/achievementServicesSpec.js new file mode 100644 index 0000000000..9d0522ccda --- /dev/null +++ b/test/spec/services/achievementServicesSpec.js @@ -0,0 +1,55 @@ +'use strict'; + +describe('achievementServices', function() { + var achievementService, rootScope; + + beforeEach(function() { + rootScope = { 'openModal': sandbox.stub() }; + module(function($provide) { + $provide.value('$rootScope', rootScope); + }); + + inject(function(Achievement) { + achievementService = Achievement; + }); + }); + + describe('#displayAchievement', function() { + it('passes given achievement name to openModal', function() { + achievementService.displayAchievement('beastMaster'); + + expect(rootScope.openModal).to.be.calledOnce; + expect(rootScope.openModal).to.be.calledWith('achievements/beastMaster'); + }); + + it('calls openModal with UserCtrl and small modal size if no other size is given', function() { + achievementService.displayAchievement('test'); + + expect(rootScope.openModal).to.be.calledOnce; + expect(rootScope.openModal).to.be.calledWith( + 'achievements/test', + { controller: 'UserCtrl', size: 'sm' } + ); + }); + + it('calls openModal with UserCtrl and specified modal size if one is given', function() { + achievementService.displayAchievement('test', {size: 'md'}); + + expect(rootScope.openModal).to.be.calledOnce; + expect(rootScope.openModal).to.be.calledWith( + 'achievements/test', + { controller: 'UserCtrl', size: 'md' } + ); + }); + + it('calls openModal with UserCtrl and default \'sm\' size if invalid size is given', function() { + achievementService.displayAchievement('test', {size: 'INVALID_SIZE'}); + + expect(rootScope.openModal).to.be.calledOnce; + expect(rootScope.openModal).to.be.calledWith( + 'achievements/test', + { controller: 'UserCtrl', size: 'sm' } + ); + }); + }); +}); diff --git a/test/spec/specHelper.js b/test/spec/specHelper.js index cbacac1988..820f05e268 100644 --- a/test/spec/specHelper.js +++ b/test/spec/specHelper.js @@ -39,7 +39,7 @@ var specHelper = {}; progress: {down: 0} } }, - preferences: {}, + preferences: { suppressModals: {} }, habits: [], dailys: [], todos: [], diff --git a/website/client/js/controllers/inventoryCtrl.js b/website/client/js/controllers/inventoryCtrl.js index acc609fef3..c4fc0ce173 100644 --- a/website/client/js/controllers/inventoryCtrl.js +++ b/website/client/js/controllers/inventoryCtrl.js @@ -1,6 +1,6 @@ habitrpg.controller("InventoryCtrl", - ['$rootScope', '$scope', 'Shared', '$window', 'User', 'Content', 'Analytics', 'Quests', 'Stats', 'Social', - function($rootScope, $scope, Shared, $window, User, Content, Analytics, Quests, Stats, Social) { + ['$rootScope', '$scope', 'Shared', '$window', 'User', 'Content', 'Analytics', 'Quests', 'Stats', 'Social', 'Achievement', + function($rootScope, $scope, Shared, $window, User, Content, Analytics, Quests, Stats, Social, Achievement) { var user = User.user; @@ -167,7 +167,7 @@ habitrpg.controller("InventoryCtrl", if(!user.achievements.beastMaster && $scope.petCount >= 90) { User.user.achievements.beastMaster = true; - $rootScope.openModal('achievements/beastMaster', {controller:'UserCtrl', size:'sm'}); + Achievement.displayAchievement('beastMaster'); } // Checks if Triad Bingo has been reached for the first time @@ -175,7 +175,7 @@ habitrpg.controller("InventoryCtrl", && $scope.mountCount >= 90 && Shared.count.dropPetsCurrentlyOwned(User.user.items.pets) >= 90) { User.user.achievements.triadBingo = true; - $rootScope.openModal('achievements/triadBingo', {controller:'UserCtrl', size:'sm'}); + Achievement.displayAchievement('triadBingo'); } } @@ -216,7 +216,7 @@ habitrpg.controller("InventoryCtrl", if(!user.achievements.mountMaster && $scope.mountCount >= 90) { User.user.achievements.mountMaster = true; - $rootScope.openModal('achievements/mountMaster', {controller:'UserCtrl', size:'sm'}); + Achievement.displayAchievement('mountMaster'); } // Selecting Pet diff --git a/website/client/js/controllers/notificationCtrl.js b/website/client/js/controllers/notificationCtrl.js index 381ff787cc..b729d0508b 100644 --- a/website/client/js/controllers/notificationCtrl.js +++ b/website/client/js/controllers/notificationCtrl.js @@ -1,8 +1,8 @@ 'use strict'; habitrpg.controller('NotificationCtrl', - ['$scope', '$rootScope', 'Shared', 'Content', 'User', 'Guide', 'Notification', 'Analytics', - function ($scope, $rootScope, Shared, Content, User, Guide, Notification, Analytics) { + ['$scope', '$rootScope', 'Shared', 'Content', 'User', 'Guide', 'Notification', 'Analytics', 'Achievement', + function ($scope, $rootScope, Shared, Content, User, Guide, Notification, Analytics, Achievement) { $rootScope.$watch('user.stats.hp', function (after, before) { if (after <= 0){ @@ -98,24 +98,24 @@ habitrpg.controller('NotificationCtrl', break; case 'WON_CHALLENGE': User.sync().then( function() { - $rootScope.openModal('wonChallenge', {controller: 'UserCtrl', size: 'sm'}); + Achievement.displayAchievement('wonChallenge'); }); break; case 'STREAK_ACHIEVEMENT': Notification.streak(User.user.achievements.streak); $rootScope.playSound('Achievement_Unlocked'); if (!User.user.preferences.suppressModals.streak) { - $rootScope.openModal('achievements/streak', {controller:'UserCtrl'}); + Achievement.displayAchievement('streak', {size: 'md'}); } break; case 'ULTIMATE_GEAR_ACHIEVEMENT': - $rootScope.openModal('achievements/ultimateGear', {controller:'UserCtrl'}); + Achievement.displayAchievement('ultimateGear', {size: 'md'}); break; case 'REBIRTH_ACHIEVEMENT': - $rootScope.openModal('achievements/rebirth', {controller:'UserCtrl', size: 'sm'}); + Achievement.displayAchievement('rebirth'); break; case 'NEW_CONTRIBUTOR_LEVEL': - $rootScope.openModal('achievements/contributor',{controller:'UserCtrl'}); + Achievement.displayAchievement('contributor', {size: 'md'}); break; case 'CRON': if (notification.data) { @@ -135,7 +135,7 @@ habitrpg.controller('NotificationCtrl', } // Since we don't use localStorage anymore, notifications for achievements and new contributor levels - // are now stored user.notifications. + // are now stored in user.notifications. $rootScope.$watchCollection('userNotifications', function (after) { if (!User.user._wrapped) return; handleUserNotifications(after); diff --git a/website/client/js/controllers/partyCtrl.js b/website/client/js/controllers/partyCtrl.js index c4cd345358..8ecd9bcaf9 100644 --- a/website/client/js/controllers/partyCtrl.js +++ b/website/client/js/controllers/partyCtrl.js @@ -1,7 +1,7 @@ 'use strict'; -habitrpg.controller("PartyCtrl", ['$rootScope','$scope','Groups','Chat','User','Challenges','$state','$compile','Analytics','Quests','Social', 'Pusher', - function($rootScope, $scope, Groups, Chat, User, Challenges, $state, $compile, Analytics, Quests, Social, Pusher) { +habitrpg.controller("PartyCtrl", ['$rootScope','$scope','Groups','Chat','User','Challenges','$state','$compile','Analytics','Quests','Social','Pusher','Achievement', + function($rootScope, $scope, Groups, Chat, User, Challenges, $state, $compile, Analytics, Quests, Social, Pusher, Achievement) { var PARTY_LOADING_MESSAGES = 4; @@ -38,14 +38,14 @@ habitrpg.controller("PartyCtrl", ['$rootScope','$scope','Groups','Chat','User',' if(!user.achievements.partyUp && $scope.group.memberCount >= 2) { User.set({'achievements.partyUp':true}); - $rootScope.openModal('achievements/partyUp', {controller:'UserCtrl', size:'sm'}); + Achievement.displayAchievement('partyUp'); } // Checks if user's party has reached 4 players for the first time. if(!user.achievements.partyOn && $scope.group.memberCount >= 4) { User.set({'achievements.partyOn':true}); - $rootScope.openModal('achievements/partyOn', {controller:'UserCtrl', size:'sm'}); + Achievement.displayAchievement('partyOn'); } } diff --git a/website/client/js/services/achievementServices.js b/website/client/js/services/achievementServices.js new file mode 100644 index 0000000000..0fab2eb161 --- /dev/null +++ b/website/client/js/services/achievementServices.js @@ -0,0 +1,28 @@ +'use strict'; + +/** + * Services that handle achievement logic. + */ + +angular.module('habitrpg').factory('Achievement', +['$rootScope', function($rootScope) { + var sizes = ['sm', 'md', 'lg']; + var DEFAULT_SIZE = 'sm'; + + function displayAchievement(achievementName, options) { + options = options || {}; + + if (options.size && sizes.indexOf(options.size) === -1) { + delete options.size; + } + + $rootScope.openModal('achievements/' + achievementName, { + controller: 'UserCtrl', + size: options.size || DEFAULT_SIZE + }); + } + + return { + displayAchievement: displayAchievement + }; +}]); diff --git a/website/client/manifest.json b/website/client/manifest.json index 3b2756d188..9ff6153b8a 100644 --- a/website/client/manifest.json +++ b/website/client/manifest.json @@ -60,6 +60,7 @@ "js/services/userServices.js", "js/services/hallServices.js", "js/services/pusherService.js", + "js/services/achievementServices.js", "js/filters/money.js", "js/filters/roundLargeNumbers.js", diff --git a/website/views/shared/modals/won-challenge.jade b/website/views/shared/modals/won-challenge.jade index a8bbc0d8b1..6285ae64dd 100644 --- a/website/views/shared/modals/won-challenge.jade +++ b/website/views/shared/modals/won-challenge.jade @@ -1,6 +1,6 @@ include ../avatar/generated_avatar -script(type='text/ng-template', id='modals/wonChallenge.html') +script(type='text/ng-template', id='modals/achievements/wonChallenge.html') - var tweet = env.t('wonChallengeShare'); .modal-content(style='min-width:28em') .modal-body.text-center