diff --git a/js/ext/angular/src/directive/ionicTabBar.js b/js/ext/angular/src/directive/ionicTabBar.js index 6ab288f2231..3bd9ee7c6ab 100644 --- a/js/ext/angular/src/directive/ionicTabBar.js +++ b/js/ext/angular/src/directive/ionicTabBar.js @@ -1,4 +1,4 @@ -angular.module('ionic.ui.tabs', ['ionic.service.view', 'ngSanitize']) +angular.module('ionic.ui.tabs', ['ionic.service.view']) /** * @description @@ -9,303 +9,274 @@ angular.module('ionic.ui.tabs', ['ionic.service.view', 'ngSanitize']) .run(['$ionicViewService', function($ionicViewService) { // set that the tabs directive should not animate when transitioning - // to it. Instead, the children directives would animate + // to it. Instead, the children directives would animate $ionicViewService.disableRegisterByTagName('ion-tabs'); }]) -.controller('$ionicTabs', ['$scope', '$ionicViewService', function($scope, $ionicViewService) { - var _this = this; +.controller('$ionicTabs', ['$scope', '$ionicViewService', '$element', function($scope, $ionicViewService, $element) { + var self = $scope.tabsController = this; + self.tabs = []; - $scope.tabCount = 0; - $scope.selectedIndex = -1; - $scope.$enableViewRegister = false; + self.selectedTab = null; - angular.extend(this, ionic.controllers.TabBarController.prototype); - - ionic.controllers.TabBarController.call(this, { - controllerChanged: function(oldC, oldI, newC, newI) { - $scope.controllerChanged && $scope.controllerChanged({ - oldController: oldC, - oldIndex: oldI, - newController: newC, - newIndex: newI - }); - }, - tabBar: { - tryTabSelect: function() {}, - setSelectedItem: function(index) {}, - addItem: function(item) {} - } - }); - - this.add = function(tabScope) { - tabScope.tabIndex = $scope.tabCount; - this.addController(tabScope); - if(tabScope.tabIndex === 0) { - this.select(0); + self.add = function(tab) { + $ionicViewService.registerHistory(tab); + self.tabs.push(tab); + if(self.tabs.length === 1) { + self.select(tab); } - $scope.tabCount++; }; - function controllerByTabIndex(tabIndex) { - for (var x=0; x<_this.controllers.length; x++) { - if (_this.controllers[x].tabIndex === tabIndex) { - return _this.controllers[x]; + self.remove = function(tab) { + var tabIndex = self.tabs.indexOf(tab); + if (tabIndex === -1) { + return; + } + //Use a field like '$tabSelected' so developers won't accidentally set it in controllers etc + if (tab.$tabSelected) { + self.deselect(tab); + //Try to select a new tab if we're removing a tab + if (self.tabs.length === 1) { + //do nothing if there are no other tabs to select + } else { + //Select previous tab if it's the last tab, else select next tab + var newTabIndex = tabIndex === self.tabs.length - 1 ? tabIndex - 1 : tabIndex + 1; + self.select(self.tabs[newTabIndex]); } } - } + self.tabs.splice(tabIndex, 1); + }; - this.select = function(tabIndex, emitChange) { - if(tabIndex !== $scope.selectedIndex) { + self.getTabIndex = function(tab) { + return self.tabs.indexOf(tab); + }; - $scope.selectedIndex = tabIndex; - $scope.activeAnimation = $scope.animation; - _this.selectController(tabIndex); + self.deselect = function(tab) { + if (tab.$tabSelected) { + self.selectedTab = null; + tab.$tabSelected = false; + (tab.onDeselect || angular.noop)(); + } + }; - var viewData = { - type: 'tab', - typeIndex: tabIndex - }; + $scope.select = self.select = function(tab, shouldEmitEvent) { + var tabIndex; + if (angular.isNumber(tab)) { + tabIndex = tab; + tab = self.tabs[tabIndex]; + } else { + tabIndex = self.tabs.indexOf(tab); + } + if (!tab || tabIndex == -1) { + throw new Error('Cannot select tab "' + tabIndex + '"!'); + } - var tabController = controllerByTabIndex(tabIndex); - if (tabController) { - viewData.title = tabController.title; - viewData.historyId = tabController.$historyId; - viewData.url = tabController.url; - viewData.uiSref = tabController.viewSref; - viewData.navViewName = tabController.navViewName; - viewData.hasNavView = tabController.hasNavView; + if (self.selectedTab && self.selectedTab.$historyId == tab.$historyId) { + if (shouldEmitEvent) { + $ionicViewService.goToHistoryRoot(tab.$historyId); } + } else { + angular.forEach(self.tabs, function(tab) { + self.deselect(tab); + }); - if(emitChange) { + self.selectedTab = tab; + //Use a funny name like $tabSelected so the developer doesn't overwrite the var in a child scope + tab.$tabSelected = true; + (tab.onSelect || angular.noop)(); + + if (shouldEmitEvent) { + var viewData = { + type: 'tab', + tabIndex: tabIndex, + historyId: tab.$historyId, + navViewName: tab.navViewName, + hasNavView: !!tab.navViewName, + title: tab.title, + //Skip the first character of href if it's # + url: tab.href, + uiSref: tab.uiSref + }; $scope.$emit('viewState.changeHistory', viewData); } - } else if(emitChange) { - var currentView = $ionicViewService.getCurrentView(); - if (currentView) { - $ionicViewService.goToHistoryRoot(currentView.historyId); - } } }; - - $scope.controllers = this.controllers; - - $scope.tabsController = this; - }]) -.directive('ionTabs', ['$ionicViewService', function($ionicViewService) { +.directive('ionTabs', ['$ionicViewService', '$ionicBind', function($ionicViewService, $ionicBind) { return { restrict: 'E', replace: true, scope: true, transclude: true, controller: '$ionicTabs', - template: '
', - compile: function(element, attr, transclude, tabsCtrl) { - return function link($scope, $element, $attr) { - - var tabs = $element[0].querySelector('.tabs'); - - $scope.tabsType = $attr.tabsType || 'tabs-positive'; - $scope.tabsStyle = $attr.tabsStyle; - $scope.animation = $attr.animation; + template: + '
' + + '
' + + '
' + + '
', + compile: function(element, attr, transclude) { + if(angular.isUndefined(attr.tabsType)) attr.$set('tabsType', 'tabs-positive'); - $scope.animateNav = $scope.$eval($attr.animateNav); - if($scope.animateNav !== false) { - $scope.animateNav = true; - } + return function link($scope, $element, $attr, tabsCtrl) { - $attr.$observe('tabsStyle', function(val) { - if(tabs) { - angular.element(tabs).addClass($attr.tabsStyle); - } + $ionicBind($scope, $attr, { + $animation: '@animation', + $tabsStyle: '@tabsStyle', + $tabsType: '@tabsType' }); - $attr.$observe('tabsType', function(val) { - if(tabs) { - angular.element(tabs).addClass($attr.tabsType); - } - }); + tabsCtrl.$scope = $scope; + tabsCtrl.$element = $element; + tabsCtrl.$tabsElement = angular.element($element[0].querySelector('.tabs')); - $scope.$watch('activeAnimation', function(value) { - $element.addClass($scope.activeAnimation); - }); - transclude($scope, function(cloned) { - $element.prepend(cloned); + transclude($scope, function(clone) { + $element.append(clone); }); - }; } }; }]) +.controller('$ionicTab', ['$scope', '$ionicViewService', '$rootScope', '$element', +function($scope, $ionicViewService, $rootScope, $element) { + this.$scope = $scope; +}]) + // Generic controller directive -.directive('ionTab', ['$ionicViewService', '$rootScope', '$parse', '$interpolate', function($ionicViewService, $rootScope, $parse, $interpolate) { +.directive('ionTab', ['$rootScope', '$animate', '$ionicBind', '$compile', '$ionicViewService', function($rootScope, $animate, $ionicBind, $compile, $ionicViewService) { + + //Returns ' key="value"' if value exists + function attrStr(k,v) { + return angular.isDefined(v) ? ' ' + k + '="' + v + '"' : ''; + } return { restrict: 'E', - require: '^ionTabs', - scope: true, + require: ['^ionTabs', 'ionTab'], + replace: true, transclude: 'element', + controller: '$ionicTab', + scope: true, compile: function(element, attr, transclude) { - - return function link($scope, $element, $attr, tabsCtrl) { - var childScope, childElement; - - $ionicViewService.registerHistory($scope); - - $scope.title = $attr.title; - $scope.icon = $attr.icon; - $scope.iconOn = $attr.iconOn; - $scope.iconOff = $attr.iconOff; - $scope.viewSref = $attr.uiSref; - $scope.url = $attr.href; - if($scope.url && $scope.url.indexOf('#') === 0) { - $scope.url = $scope.url.replace('#', ''); - } - - // Should we hide a back button when this tab is shown - $scope.hideBackButton = $scope.$eval($attr.hideBackButton); - - if($scope.hideBackButton !== true) { - $scope.hideBackButton = false; - } - - // Whether we should animate on tab change, also impacts whether we - // tell any parent nav controller to animate - $scope.animate = $scope.$eval($attr.animate); - - var badgeGet = $parse($attr.badge); - $scope.$watch(badgeGet, function(value) { - $scope.badge = value; - }); - - $attr.$observe('badgeStyle', function(value) { - $scope.badgeStyle = value; + return function link($scope, $element, $attr, ctrls) { + var childScope, childElement, tabNavElement; + tabsCtrl = ctrls[0], + tabCtrl = ctrls[1]; + + $ionicBind($scope, $attr, { + animate: '=', + leftButtons: '=', + rightButtons: '=', + onSelect: '&', + onDeselect: '&', + title: '@', + uiSref: '@', + href: '@', }); - var leftButtonsGet = $parse($attr.leftButtons); - $scope.$watch(leftButtonsGet, function(value) { - $scope.leftButtons = value; - if($scope.doesUpdateNavRouter) { - $scope.$emit('viewState.leftButtonsChanged', $scope.rightButtons); - } - }); - - var rightButtonsGet = $parse($attr.rightButtons); - $scope.$watch(rightButtonsGet, function(value) { - $scope.rightButtons = value; - }); + tabNavElement = angular.element( + '' + ); + tabNavElement.data('$ionTabsController', tabsCtrl); + tabNavElement.data('$ionTabController', tabCtrl); + tabsCtrl.$tabsElement.append($compile(tabNavElement)($scope)); tabsCtrl.add($scope); + $scope.$on('$destroy', function() { + tabsCtrl.remove($scope); + tabNavElement.isolateScope().$destroy(); + tabNavElement.remove(); + }); - function cleanupChild() { - if(childElement) { - childElement.remove(); - childElement = null; - } - if(childScope) { - childScope.$destroy(); - childScope = null; + $scope.$watch('$tabSelected', function(value) { + if (!value) { + $scope.$broadcast('tab.hidden', $scope); } - } - - $scope.$watch('isVisible', function(value) { + childScope && childScope.$destroy(); + childScope = null; + childElement && $animate.leave(childElement); + childElement = null; if (value) { - cleanupChild(); childScope = $scope.$new(); transclude(childScope, function(clone) { - clone.addClass('view'); - clone.removeAttr('title'); + //remove title attr to stop hover annoyance! + clone[0].removeAttribute('title'); + $animate.enter(clone, tabsCtrl.$element); + clone.addClass('pane'); childElement = clone; - $element.parent().append(childElement); }); - $scope.$broadcast('tab.shown'); - } else if (childScope) { - $scope.$broadcast('tab.hidden'); - cleanupChild(); + $scope.$broadcast('tab.shown', $scope); } }); - // on link, check if it has a nav-view in it - transclude($scope.$new(), function(clone) { - var navViewEle = clone[0].getElementsByTagName("ion-nav-view"); - $scope.hasNavView = (navViewEle.length > 0); - if($scope.hasNavView) { - // this tab has a ui-view - $scope.navViewName = navViewEle[0].getAttribute('name'); - if( $ionicViewService.isCurrentStateNavView( $scope.navViewName ) ) { - // this tab's ui-view is the current one, go to it! - tabsCtrl.select($scope.tabIndex); - } + transclude($scope, function(clone) { + var navView = clone[0].querySelector('ion-nav-view'); + if (navView) { + $scope.navViewName = navView.getAttribute('name'); + selectTabIfMatchesState(); + $scope.$on('$stateChangeSuccess', selectTabIfMatchesState); } }); - var unregister = $rootScope.$on('$stateChangeSuccess', function(value){ - if( $ionicViewService.isCurrentStateNavView($scope.navViewName) && - $scope.tabIndex !== tabsCtrl.selectedIndex) { - tabsCtrl.select($scope.tabIndex); + function selectTabIfMatchesState() { + // this tab's ui-view is the current one, go to it! + if ($ionicViewService.isCurrentStateNavView($scope.navViewName)) { + tabsCtrl.select($scope); } - }); - - $scope.$on('$destroy', unregister); - + } }; } }; }]) - -.directive('ionTabControllerBar', function() { +.directive('ionTabNav', function() { return { restrict: 'E', - require: '^ionTabs', - transclude: true, replace: true, - scope: true, - template: '
' + - '' + + require: ['^ionTabs', '^ionTab'], + template: + '
' + + '{{badge}}' + + '' + + '' + + '' + '
', - link: function($scope, $element, $attr, tabsCtrl) { - $element.addClass($scope.tabsType); - $element.addClass($scope.tabsStyle); - } - }; -}) - -.directive('ionTabControllerItem', ['$window', function($window) { - return { - restrict: 'E', - replace: true, - require: '^ionTabs', scope: { - iconTitle: '@', + title: '@', icon: '@', iconOn: '@', iconOff: '@', + uiSref: '@', + href: '@', badge: '=', - badgeStyle: '=', - active: '=', - tabSelected: '@', - index: '=' + badgeStyle: '@' }, - link: function(scope, element, attrs, tabsCtrl) { - if(attrs.icon) { - scope.iconOn = scope.iconOff = attrs.icon; + compile: function(element, attr, transclude) { + if (attr.icon) { + attr.$set('iconOn', attr.icon); + attr.$set('iconOff', attr.icon); } - - scope.selectTab = function() { - tabsCtrl.select(scope.index, true); + return function link($scope, $element, $attrs, ctrls) { + var tabsCtrl = ctrls[0], + tabCtrl = ctrls[1]; + + $scope.isTabActive = function() { + return tabsCtrl.selectedTab === tabCtrl.$scope; + }; + $scope.selectTab = function() { + tabsCtrl.select(tabCtrl.$scope, true); + }; }; - }, - template: - '' + - '{{badge}}' + - '' + - '' + - '' + - '' + - '' + } }; -}]); - +}); diff --git a/js/ext/angular/test/directive/ionicTabBar.unit.js b/js/ext/angular/test/directive/ionicTabBar.unit.js index 59a8de2c73c..dfc3a1bc605 100644 --- a/js/ext/angular/test/directive/ionicTabBar.unit.js +++ b/js/ext/angular/test/directive/ionicTabBar.unit.js @@ -1,309 +1,432 @@ describe('tabs', function() { - beforeEach(module('ionic.ui.tabs')); - describe('$ionicTabs controller', function() { + describe('miscellaneous', function() { + beforeEach(module('ionic', function($provide) { + $provide.value('$ionicViewService', { + disableRegisterByTagName: jasmine.createSpy('disableRegisterByTagName') + }); + })); + it('should register tabs', inject(function($ionicViewService) { + expect($ionicViewService.disableRegisterByTagName).toHaveBeenCalledWith('ion-tabs'); + })); + }); + describe('$ionicTabs controller', function() { + beforeEach(module('ionic')); var ctrl, scope; beforeEach(inject(function($rootScope, $controller) { scope = $rootScope.$new(); ctrl = $controller('$ionicTabs', { - $scope: scope + $scope: scope, + $element: angular.element('
') }); })); - it('select should change getSelectedControllerIndex', function() { - // Verify no items selected - expect(ctrl.getSelectedControllerIndex()).toBeUndefined(); - expect(scope.selectedIndex).toBe(-1); + it('should add itself to scope', function() { + expect(scope.tabsController).toBe(ctrl); + }); - // Try selecting beyond the bounds - ctrl.selectController(1); - expect(ctrl.getSelectedControllerIndex()).toBeUndefined(); - expect(scope.selectedIndex).toBe(-1); + it('.getTabIndex should return indexOf tab', function() { + ctrl.tabs = [1,2]; + expect(ctrl.getTabIndex(1)).toBe(0); + expect(ctrl.getTabIndex(2)).toBe(1); + expect(ctrl.getTabIndex(3)).toBe(-1); + }); - // Add a controller - ctrl.add({ - title: 'Cats', - icon: 'icon-kitty-kat' - }); + it('.add should add tab and select if empty, & set historyId', inject(function($ionicViewService) { + var tab1 = {}; + var tab2 = {}; + spyOn($ionicViewService, 'registerHistory'); + spyOn(ctrl, 'select'); + + ctrl.add(tab1); + expect($ionicViewService.registerHistory).toHaveBeenCalledWith(tab1); + expect(ctrl.tabs).toEqual([tab1]); + expect(ctrl.select).toHaveBeenCalledWith(tab1); + + ctrl.select.reset(); + ctrl.add(tab2); + expect($ionicViewService.registerHistory).toHaveBeenCalledWith(tab2); + expect(ctrl.tabs).toEqual([tab1, tab2]); + expect(ctrl.select).not.toHaveBeenCalled(); + })); - expect(ctrl.getSelectedControllerIndex()).toEqual(0); - expect(scope.selectedIndex).toBe(0); + it('.remove should remove tab and reselect', function() { + var tab1 = {}, tab2 = {}, tab3 = {}; + ctrl.add(tab1); + ctrl.add(tab2); + ctrl.add(tab3); + expect(ctrl.selectedTab).toBe(tab1); + + ctrl.select(tab3); + expect(ctrl.selectedTab).toBe(tab3); + + ctrl.remove(tab3); + expect(ctrl.selectedTab).toBe(tab2); + expect(ctrl.tabs.indexOf(tab3)).toBe(-1); + ctrl.remove(tab1); + expect(ctrl.selectedTab).toBe(tab2); + expect(ctrl.tabs.indexOf(tab1)).toBe(-1); + ctrl.remove(tab2) + expect(ctrl.selectedTab).toBe(null); + expect(ctrl.tabs.indexOf(tab2)).toBe(-1); + expect(ctrl.tabs.length).toBe(0); + }); - ctrl.add({ - title: 'Cats', - icon: 'icon-kitty-kat' - }); + it('.deselect should unselect if visible', function() { + var tab1 = { + $tabSelected: true, + onDeselect: jasmine.createSpy('deselect') + }; + ctrl.selectedTab = tab1; + ctrl.deselect(tab1); + expect(tab1.$tabSelected).toBe(false); + expect(tab1.onDeselect).toHaveBeenCalled(); + expect(ctrl.selectedTab).toBe(null); + }); - expect(ctrl.getSelectedControllerIndex()).toEqual(0); - expect(scope.selectedIndex).toBe(0); + it('.deselect should do nothing if not visible', function() { + var tab1 = { + $tabSelected: false, + onDeselect: jasmine.createSpy('deselect') + }; + ctrl.selectedTab = 'foo'; + ctrl.deselect(tab1); + expect(tab1.$tabSelected).toBe(false); + expect(tab1.onDeselect).not.toHaveBeenCalled(); + expect(ctrl.selectedTab).toBe('foo'); + }); - ctrl.select(1); + it('.select should throw error if tab doesnt exist', function() { + var tab = {}; + ctrl.add(tab); + expect(function() { + ctrl.select({}); + }).toThrow(); + expect(function() { + ctrl.select(null); + }).toThrow(); + expect(function() { + ctrl.select(tab); + }).not.toThrow(); + }); - expect(ctrl.getSelectedControllerIndex()).toEqual(1); - expect(scope.selectedIndex).toBe(1); + it('.select should throw error if number is bad', function() { + ctrl.add({}); + expect(function() { + ctrl.select(1); + }).toThrow(); + expect(function() { + ctrl.select(-1); + }).toThrow(); + expect(function() { + ctrl.select(0); + }).not.toThrow(); }); - it('select should emit viewData if emit is passed in', function() { - ctrl.add({ title: 'foo', icon: 'icon' }); - ctrl.add({ title: 'bar', icon: 'icon2' }); + it('.select should allow number', function() { + var tab1 = {}, tab2 = {}; + ctrl.add(tab1); + ctrl.add(tab2); - var viewData; - spyOn(scope, '$emit').andCallFake(function(e, data) { - viewData = data; - }); + ctrl.select(tab2); + expect(ctrl.selectedTab).toBe(tab2); ctrl.select(0); - expect(scope.$emit).not.toHaveBeenCalled(); + expect(ctrl.selectedTab).toBe(tab1); + + ctrl.select(1); + expect(ctrl.selectedTab).toBe(tab2); - ctrl.select(1, true); - expect(scope.$emit).toHaveBeenCalledWith('viewState.changeHistory', jasmine.any(Object)); - expect(viewData).toBeTruthy(); - expect(viewData.type).toBe('tab'); - expect(viewData.typeIndex).toBe(1); - expect(viewData.title).toBe('bar'); + ctrl.select(tab1); + expect(ctrl.selectedTab).toBe(tab1); }); - it('select should go to root if emit is true and selecting same tab index', inject(function($ionicViewService) { - ctrl.add({ title: 'foo', icon: 'icon' }); + it('.select on selected tab should do nothing or go to history root', inject(function($ionicViewService) { spyOn($ionicViewService, 'goToHistoryRoot'); - spyOn($ionicViewService, 'getCurrentView').andCallFake(function() { - return { historyId:'001' }; - }); + var tab = { $historyId: '1' }; + ctrl.add(tab); + expect(ctrl.selectedTab).toBe(tab); - expect(scope.selectedIndex).toBe(0); - //Emit != true - ctrl.select(0); + //Do nothing unless emit event is passed + ctrl.select(tab); expect($ionicViewService.goToHistoryRoot).not.toHaveBeenCalled(); - ctrl.select(0, true); - expect($ionicViewService.goToHistoryRoot).toHaveBeenCalledWith('001'); + ctrl.select(tab, true); + expect($ionicViewService.goToHistoryRoot).toHaveBeenCalledWith(tab.$historyId); })); - it('select should call change callback', function() { - scope.onControllerChanged = function(oldC, oldI, newC, newI) { - }; - - // Add a controller - ctrl.add({ title: 'Cats', icon: 'icon-kitty-kat' }); - ctrl.add({ title: 'Dogs', icon: 'icon-rufus' }); - spyOn(ctrl, 'controllerChanged'); + it('.select should deselect all other tabs and set selected', function() { + var tab1 = {}, tab2 = { + onSelect: jasmine.createSpy('select') + }; + ctrl.add(tab1); + ctrl.add(tab2); + spyOn(ctrl, 'deselect'); - expect(ctrl.getSelectedControllerIndex()).toEqual(0); - ctrl.select(1); + ctrl.select(tab2); + expect(ctrl.deselect).toHaveBeenCalledWith(tab1); + expect(ctrl.deselect).toHaveBeenCalledWith(tab2); - expect(ctrl.controllerChanged).toHaveBeenCalled(); + expect(tab2.$tabSelected).toBe(true); + expect(ctrl.selectedTab).toBe(tab2); + expect(tab2.onSelect).toHaveBeenCalled(); }); - it('select should change activeAnimation=animation', function() { - // Add a controller - ctrl.add({ title: 'Cats', icon: 'icon-kitty-kat' }); - ctrl.add({ title: 'Dogs', icon: 'icon-rufus' }); - expect(scope.activeAnimation).toBeUndefined(); - scope.animation = 'superfast'; - ctrl.select(1); - expect(scope.activeAnimation).toBe('superfast'); + it('.select with true should emit event', function() { + var tab1 = {}; + var tab2 = {}; + var tab3 = { + navViewName: 'viewName', + hasNavView: true, + title: 'Super Tab', + url: 'ionicframework.com', + uiSref: 'drifty' + }; + var eName, eData; + spyOn(scope, '$emit').andCallFake(function(eventName, data) { + eName = eventName; + eData = data; + }); + ctrl.add(tab1); + ctrl.add(tab2); + ctrl.add(tab3); - scope.animation = 'woah'; - ctrl.select(0); - expect(scope.activeAnimation).toBe('woah'); + ctrl.select(tab2); + expect(scope.$emit).not.toHaveBeenCalled(); + ctrl.select(tab3, true); + expect(scope.$emit).toHaveBeenCalled(); + expect(eName).toBe('viewState.changeHistory'); + expect(eData).toEqual({ + type: 'tab', + tabIndex: 2, + historyId: tab3.$historyId, + navViewName: tab3.navViewName, + hasNavView: tab3.hasNavView, + title: tab3.title, + url: tab3.href, + uiSref: tab3.uiSref + }); }); - }); - describe('tabs directive', function() { - var compile, scope, element; - beforeEach(inject(function($compile, $rootScope) { - compile = $compile; - scope = $rootScope; - })); - - it('Has tab class', function() { - var element = compile('')(scope); - scope.$digest(); - expect(element.find('.tabs').hasClass('tabs')).toBe(true); - }); - - it('Has tab children', function() { - element = compile('')(scope); - scope = element.scope(); - scope.controllers = [ - { title: 'Home', icon: 'icon-home' }, - { title: 'Fun', icon: 'icon-fun' }, - { title: 'Beer', icon: 'icon-beer' }, - ]; - scope.$digest(); - expect(element.find('a').length).toBe(3); - }); + describe('ionTabs directive', function() { + beforeEach(module('ionic')); + function setup(attrs, content) { + var element; + inject(function($compile, $rootScope) { + var scope = $rootScope.$new(); + element = $compile('' + (content||'') + '')(scope); + scope.$apply(); + }); + return element; + } - it('Has compiled children', function() { - element = compile('' + - '' + - '' + - '')(scope); - scope.$digest(); - expect(element.find('a').length).toBe(2); + it('should set attr classes', function() { + var el = setup('animation="foo" tabs-style="bar" tabs-type="baz"'); + expect(el.hasClass('foo')).toBe(true); + expect(el.children().hasClass('bar baz')).toBe(true); }); - it('Sets style on child tabs', function() { - element = compile('' + - '' + - '' + - '')(scope); - scope.$digest(); - var tabs = element[0].querySelector('.tabs'); - expect(angular.element(tabs).hasClass('tabs-positive')).toEqual(true); - expect(angular.element(tabs).hasClass('tabs-icon-bottom')).toEqual(true); + it('should default tabsType to tabs-positive', function() { + var el = setup(); + expect(el.children().hasClass('tabs-positive')).toBe(true); }); - it('Has nav-view', function() { - element = compile('' + - '' + - 'content2' + - '')(scope); - scope = element.scope(); - scope.$digest(); - expect(scope.tabCount).toEqual(2); - expect(scope.selectedIndex).toEqual(0); - expect(scope.controllers.length).toEqual(2); - expect(scope.controllers[0].hasNavView).toEqual(true); - expect(scope.controllers[0].navViewName).toEqual('name1'); - expect(scope.controllers[0].url).toEqual('/page1'); - expect(scope.controllers[1].hasNavView).toEqual(false); - expect(scope.controllers[1].url).toEqual('/page2'); + it('should transclude content with same scope', function() { + var el = setup('', '
'); + expect(el.children().eq(1).hasClass('content')).toBe(true); + expect(el.children().eq(1).scope()).toBe(el.scope()); }); - }); - describe('tab-item Directive', function() { + describe('ionTab directive', function() { + beforeEach(module('ionic')); + var tabsCtrl, tabsEl, scope; + function setup(attrs, content) { + inject(function($compile, $rootScope, $injector) { + tabsEl = angular.element(''+(content||'')+''); - var compile, element, scope, ctrl; - beforeEach(inject(function($compile, $rootScope, $document, $controller) { - compile = $compile; - scope = $rootScope.$new(); + $compile(tabsEl)($rootScope.$new()); + $rootScope.$apply(); - scope.badgeValue = 3; - scope.badgeStyleValue = 'badge-assertive'; - element = compile('' + - '' + - '')(scope); - scope.$digest(); - $document[0].body.appendChild(element[0]); - })); + tabsCtrl = tabsEl.data('$ionTabsController'); + scope = tabsEl.scope(); - it('Title works', function() { - //The badge's text gets in the way of just doing .text() on the element itself, so exclude it - var notBadge = angular.element(element[0].querySelectorAll('a >:not(.badge)')); - expect(notBadge.text().trim()).toEqual('Item'); - }); + spyOn(tabsCtrl, 'remove'); + }); + } - it('Default icon works', function() { - scope.$digest(); - var i = element[0].querySelectorAll('i')[0]; - expect(angular.element(i).hasClass('icon-default')).toEqual(true); + it('should add itself to tabsCtrl and remove on $destroy', function() { + var el = setup(); + var tab = tabsCtrl.tabs[0]; + tab.$destroy(); + expect(tabsCtrl.remove).toHaveBeenCalledWith(tab); }); - it('Badge works', function() { - scope.$digest(); - var i = element[0].querySelector('.badge'); - expect(i.innerHTML).toEqual('3'); - expect(i.className).toMatch('badge-assertive'); - scope.$apply("badgeStyleValue = 'badge-danger'"); - expect(i.className).toMatch('badge-danger'); + it('should compile a with all of the relevant attrs', function() { + setup('title=1 icon-on=3 icon-off=4 ui-sref=5 href=6 badge=7 badge-style=8'); + var navItem = angular.element(tabsEl[0].querySelector('.tab-item')); + expect(navItem.attr('title')).toEqual('1'); + expect(navItem.attr('icon-on')).toEqual('3'); + expect(navItem.attr('icon-off')).toEqual('4'); + expect(navItem.attr('ui-sref')).toEqual('5'); + expect(navItem.attr('href')).toEqual('6'); + expect(navItem.attr('badge')).toEqual('7'); + expect(navItem.attr('badge-style')).toEqual('8'); + + expect(navItem.parent()[0]).toBe(tabsCtrl.$tabsElement[0]); }); - it('Badge updates', function() { - scope.badgeValue = 10; - scope.$digest(); - var span = element[0].querySelectorAll('span')[0]; - expect(span.innerHTML).toEqual('10'); + it('should remove on $destroy', function() { + setup(); + var navItem = angular.element(tabsEl[0].querySelector('.tab-item')); + var navItemScope = navItem.isolateScope(); + spyOn(navItemScope, '$destroy'); + + tabsCtrl.tabs[0].$destroy(); + expect(navItemScope.$destroy).toHaveBeenCalled(); + expect(navItem.parent().length).toBe(0); }); - it('Click sets correct tab index', function() { - var a = element.find('a:eq(0)'); - var itemScope = a.isolateScope(); - //spyOn(a, 'click'); - spyOn(itemScope, 'selectTab'); - a.click(); - expect(itemScope.selectTab).toHaveBeenCalled(); + it('should not set navViewName if no child nav-view', function() { + setup(); + expect(tabsCtrl.tabs[0].navViewName).toBeUndefined(); }); - }); - describe('tab directive', function() { - var scope, tab; - beforeEach(inject(function($compile, $rootScope, $controller) { - var tabsScope = $rootScope.$new(); - //Setup a fake tabs controller for our tab to use so we dont have to have a parent tabs directive (isolated test) - var ctrl = $controller('$ionicTabs', { - $scope: tabsScope + it('should set navViewName and select when necessary if a child nav-view', inject(function($ionicViewService, $rootScope) { + var isCurrent = false; + var deregister = function(){}; + var listener; + spyOn($ionicViewService, 'isCurrentStateNavView').andCallFake(function(name) { + return isCurrent; }); - //Create an outer div that has a tabsController on it so ion-tab thinks it's in a - var element = angular.element('
'); - element.data('$tabsController', ctrl); - $compile(element)(tabsScope) - tabsScope.$apply(); + setup('', ''); + spyOn(tabsCtrl, 'select'); + var tab = tabsCtrl.tabs[0]; + + expect(tab.navViewName).toBe('banana'); + expect($ionicViewService.isCurrentStateNavView).toHaveBeenCalledWith('banana'); - tab = element.find('tab'); - scope = tab.scope(); + $ionicViewService.isCurrentStateNavView.reset(); + isCurrent = true; + $rootScope.$broadcast('$stateChangeSuccess'); + + expect($ionicViewService.isCurrentStateNavView).toHaveBeenCalledWith('banana'); + expect(tabsCtrl.select).toHaveBeenCalledWith(tab); })); - }); - describe('tab-controller-item Directive', function() { + it('should transclude on $tabSelected=true', function() { + setup('', '
'); + var tab = tabsCtrl.tabs[0]; + tabsCtrl.deselect(tab); + tab.$apply(); - var compile, element, scope, ctrl; - beforeEach(inject(function($compile, $rootScope, $document, $controller) { - compile = $compile; - scope = $rootScope; + spyOn(tab, '$broadcast'); - scope.badgeValue = 3; - scope.isActive = false; - element = compile('' + - '' + - '')(scope); - scope.$digest(); - $document[0].body.appendChild(element[0]); - })); + var tabContent = tabsEl.find('.pane'); + expect(tabContent.length).toBe(0); - it('Icon title works as html', function() { - var a = element.find('a'); - var spans = a.find('span'); - var lastSpan = spans.eq(spans.length - 1); - expect(lastSpan.html()).toEqual('Icon title'); - }); + tab.$apply('$tabSelected = true'); - it('Icon classes works', function() { - var title = ''; - var elements = element[0].querySelectorAll('.icon-class'); - expect(elements.length).toEqual(1); - var elements = element[0].querySelectorAll('.icon-off-class'); - expect(elements.length).toEqual(1); - }); + tabContent = tabsEl.find('.pane'); + expect(tabContent.parent()[0]).toBe(tabsCtrl.$element[0]); + var contentScope = tabContent.scope(); + expect(tabContent.length).toBe(1); + expect(tabContent.find('.inside-content').length).toBe(1); + expect(tab.$broadcast).toHaveBeenCalledWith('tab.shown', tab); - it('Active switch works', function() { - var elements = element[0].querySelectorAll('.icon-on-class'); - expect(elements.length).toEqual(0); + spyOn(tabContent, 'remove'); + spyOn(contentScope, '$destroy'); + tab.$broadcast.reset(); - scope.isActive = true; - scope.$digest(); + tab.$apply('$tabSelected = false'); + expect(tabContent.parent().length).toBe(0); //removed check + expect(contentScope.$destroy).toHaveBeenCalled(); + expect(tab.$broadcast).toHaveBeenCalledWith('tab.hidden', tab); + }); - var elements = element[0].querySelectorAll('.icon-on-class'); - expect(elements.length).toEqual(1); + }); + + describe('ionTabNav directive', function() { + beforeEach(module('ionic')); + var tabsCtrl, tabCtrl; + function setup(attrs) { + tabsCtrl = { + select: jasmine.createSpy('select') + }; + tabCtrl = { + $scope: {} + }; + var element; + inject(function($compile, $rootScope) { + var scope = $rootScope.$new(); + element = angular.element(''); + element.data('$ionTabsController', tabsCtrl); + element.data('$ionTabController', tabCtrl); + element = $compile(element)(scope); + scope.$apply(); + }); + return element; + } + + // These next two are REALLY specific unit tests, + // but also are really really vital pieces of code + it('.isTabActive should be correct', function() { + var el = setup(); + expect(el.isolateScope().isTabActive()).toBe(false); + tabsCtrl.selectedTab = tabCtrl.$scope; + expect(el.isolateScope().isTabActive()).toBe(true); + tabsCtrl.selectedTab = null; + expect(el.isolateScope().isTabActive()).toBe(false); + }); + it('.selectTab should be correct', function() { + var el = setup(); + el.isolateScope().selectTab(); + expect(tabsCtrl.select).toHaveBeenCalledWith(tabCtrl.$scope, true); }); - it('Badge updates', function() { - scope.badgeValue = 10; - scope.badgeStyle = 'badge-assertive'; - scope.$digest(); - var i = element[0].querySelector('.badge'); - expect(i.innerHTML).toEqual('10'); - expect(i.className).toMatch('badge-assertive'); - scope.$apply('badgeStyle = "badge-super"'); - expect(i.className).toMatch('badge-super'); + it('should set iconOn and iconOff to icon if icon is given', function() { + var el = setup('icon=1'); + expect(el.attr('icon-on')).toBe('1'); + expect(el.attr('icon-off')).toBe('1'); }); + it('should select tab on click', function() { + var el = setup(); + el.triggerHandler('click'); + expect(tabsCtrl.select).toHaveBeenCalledWith(tabCtrl.$scope, true); + }); + it('should have title', function() { + var el = setup('title="hi, {{name}}!"'); + expect(el.find('.tab-title').html()).toBe('hi, !'); + el.scope().$apply('name = "joe"'); + expect(el.find('.tab-title').html()).toBe('hi, joe!'); + }); + it('should change classes based on active', function() { + var el = setup('icon-on=on icon-off=off'); + + el.isolateScope().isTabActive = function() { return true; }; + el.isolateScope().$apply(); + expect(el.hasClass('active')).toBe(true); + expect(el.find('.icon.on').hasClass('ng-hide')).toBe(false); + expect(el.find('.icon.off').hasClass('ng-hide')).toBe(true); + + el.isolateScope().isTabActive = function() { return false; }; + el.isolateScope().$apply(); + expect(el.hasClass('active')).toBe(false); + expect(el.find('.icon.on').hasClass('ng-hide')).toBe(true); + expect(el.find('.icon.off').hasClass('ng-hide')).toBe(false); + }); + it('shouldnt has-badge without badge', function() { + expect(setup().hasClass('has-badge')).toBe(false); + }); + it('should have badge', function() { + var el = setup('badge="\'badger\'" badge-style="super-style"'); + expect(el.hasClass('has-badge')).toBe(true); + expect(el.find('.badge.super-style').text()).toBe('badger'); + }); }); }); - -