From a402415a2a28b360c43b9fe8f4f54c540f6c33de Mon Sep 17 00:00:00 2001 From: Nate Abele Date: Fri, 27 Dec 2013 02:41:28 -0500 Subject: [PATCH] fix(uiView): test pass against 1.0.8 and 1.2.4 --- files.js | 4 +- src/viewDirective.js | 181 +++++++++++++++++++++--------------- test/compat/matchers.js | 8 +- test/stateDirectivesSpec.js | 2 +- test/viewDirectiveSpec.js | 143 +++++++++++++++++----------- 5 files changed, 200 insertions(+), 138 deletions(-) diff --git a/files.js b/files.js index fb1f789eb..df1795ccc 100644 --- a/files.js +++ b/files.js @@ -25,8 +25,8 @@ routerFiles = { angular: function(version) { return [ 'lib/angular-' + version + '/angular.js', - 'lib/angular-' + version + '/angular-mocks.js', - ]; + 'lib/angular-' + version + '/angular-mocks.js' + ].concat(version === '1.2.4' ? ['lib/angular-' + version + '/angular-animate.js'] : []); } }; diff --git a/src/viewDirective.js b/src/viewDirective.js index b9f3feba9..f2aea8859 100644 --- a/src/viewDirective.js +++ b/src/viewDirective.js @@ -15,8 +15,8 @@ * * @param {string} ui-view A view name. */ -$ViewDirective.$inject = ['$state', '$compile', '$controller', '$injector', '$uiViewScroll']; -function $ViewDirective( $state, $compile, $controller, $injector, $uiViewScroll) { +$ViewDirective.$inject = ['$state', '$compile', '$controller', '$injector', '$uiViewScroll', '$document']; +function $ViewDirective( $state, $compile, $controller, $injector, $uiViewScroll, $document) { function getService() { return ($injector.has) ? function(service) { @@ -30,118 +30,149 @@ function $ViewDirective( $state, $compile, $controller, $injector, $ui }; } - var viewIsUpdating = false, service = getService(), - $animator = service('$animator'), $animate = service('$animate'), - hasAnimator = !!($animator || $animate); + var viewIsUpdating = false, + service = getService(), + $animator = service('$animator'), + $animate = service('$animate'); // Returns a set of DOM manipulation functions based on whether animation // should be performed - var renderer = function(shouldAnimate) { - return (hasAnimator && shouldAnimate) ? { - remove: function(element) { $animate.leave(element.contents()); }, - // remove: function(element) { animate.leave(element.contents(), element); }, - restore: function(compiled, element) { $animate.enter(compiled, element); }, - // restore: function(compiled, element) { animate.enter(compiled, element); }, - populate: function(template, element) { - var contents = angular.element('
').html(template).contents(); - // animate.enter(contents, element); - $animate.enter(contents, element); - return contents; - } - } : { - remove: function(element) { element.html(''); }, - restore: function(compiled, element) { element.append(compiled); }, - populate: function(template, element) { - element.html(template); - return element.contents(); - } + function getRenderer(element, attrs, scope) { + var statics = function() { + return { + leave: function (element) { element.remove(); }, + enter: function (element, parent, anchor) { anchor.after(element); } + }; }; - }; + + if ($animate) { + return function(shouldAnimate) { + return !shouldAnimate ? statics() : { + enter: function(element, parent, anchor) { $animate.enter(element, null, anchor); }, + leave: function(element) { $animate.leave(element, function() { element.remove(); }); } + }; + }; + } + + if ($animator) { + var animate = $animator && $animator(scope, attrs); + + return function(shouldAnimate) { + return !shouldAnimate ? statics() : { + enter: function(element, parent, anchor) { animate.enter(element, parent); }, + leave: function(element) { animate.leave(element.contents(), element); } + }; + }; + } + + return statics; + } var directive = { restrict: 'ECA', - terminal: true, - priority: 1000, - transclude: true, - compile: function (element, attr, transclude) { - return function(scope, element, attr) { - var viewScope, viewLocals, - name = attr[directive.name] || attr.name || '', - onloadExp = attr.onload || '', - autoscrollExp = attr.autoscroll, - animate = $animator && $animator(scope, attr), - initialView = transclude(scope); - - // Put back the compiled initial view - element.append(initialView); - - // Find the details of the parent view directive (if any) and use it - // to derive our own qualified view name, then hang our own details - // off the DOM so child directives can find it. - var parent = element.parent().inheritedData('$uiView'); - if (name.indexOf('@') < 0) name = name + '@' + (parent ? parent.state.name : ''); + compile: function (element, attrs) { + var initial = element.html(), + isDefault = true, + anchor = angular.element($document[0].createComment(' ui-view-anchor ')), + parentEl = element.parent(); + + element.prepend(anchor); + + return function ($scope) { + var inherited = parentEl.inheritedData('$uiView'); + + var currentScope, currentEl, viewLocals, + name = attrs[directive.name] || attrs.name || '', + onloadExp = attrs.onload || '', + autoscrollExp = attrs.autoscroll, + renderer = getRenderer(element, attrs, $scope); + + if (name.indexOf('@') < 0) name = name + '@' + (inherited ? inherited.state.name : ''); var view = { name: name, state: null }; - element.data('$uiView', view); - var eventHook = function() { + var eventHook = function () { if (viewIsUpdating) return; viewIsUpdating = true; - try { updateView(true); } catch (e) { + try { updateView(); } catch (e) { viewIsUpdating = false; throw e; } viewIsUpdating = false; }; - scope.$on('$stateChangeSuccess', eventHook); - scope.$on('$viewContentLoading', eventHook); - updateView(false); + $scope.$on('$stateChangeSuccess', eventHook); + $scope.$on('$viewContentLoading', eventHook); - function updateView(doAnimate) { - var locals = $state.$current && $state.$current.locals[name]; - if (locals === viewLocals) return; // nothing to do - var render = renderer(doAnimate); + updateView(); - // Remove existing content - render.remove(element); + function cleanupLastView() { + if (currentEl) { + renderer(true).leave(currentEl); + currentEl = null; + } - // Destroy previous view scope - if (viewScope) { - viewScope.$destroy(); - viewScope = null; + if (currentScope) { + currentScope.$destroy(); + currentScope = null; } + } - if (!locals) { - viewLocals = null; - view.state = null; + function updateView() { + var locals = $state.$current && $state.$current.locals[name]; + + if (isDefault) { + isDefault = false; + element.replaceWith(anchor); + } - // Restore the initial view - return render.restore(initialView, element); + if (!locals) { + cleanupLastView(); + currentEl = element.clone(); + currentEl.html(initial); + anchor.after(currentEl); + + currentScope = $scope.$new(); + $compile(currentEl.contents())(currentScope); + return; } + if (locals === viewLocals) return; // nothing to do + + cleanupLastView(); + + currentEl = element.clone(); + currentEl.html(locals.$template ? locals.$template : initial); + renderer(true).enter(currentEl, parentEl, anchor); + + currentEl.data('$uiView', view); + viewLocals = locals; view.state = locals.$$state; - var link = $compile(render.populate(locals.$template, element)); - viewScope = scope.$new(); + var link = $compile(currentEl.contents()); + + currentScope = $scope.$new(); if (locals.$$controller) { - locals.$scope = viewScope; + locals.$scope = currentScope; var controller = $controller(locals.$$controller, locals); - element.children().data('$ngControllerController', controller); + currentEl.children().data('$ngControllerController', controller); } - link(viewScope); - viewScope.$emit('$viewContentLoaded'); - if (onloadExp) viewScope.$eval(onloadExp); - if (!angular.isDefined(autoscrollExp) || !autoscrollExp || scope.$eval(autoscrollExp)) { - $uiViewScroll(element); + link(currentScope); + + currentScope.$emit('$viewContentLoaded'); + if (onloadExp) currentScope.$eval(onloadExp); + + if (!angular.isDefined(autoscrollExp) || !autoscrollExp || $scope.$eval(autoscrollExp)) { + $uiViewScroll(currentEl); } } }; } }; + return directive; } diff --git a/test/compat/matchers.js b/test/compat/matchers.js index 2369380b1..db1d1dcfa 100644 --- a/test/compat/matchers.js +++ b/test/compat/matchers.js @@ -149,15 +149,11 @@ beforeEach(function() { angular.element(this.actual).hasClass(clazz); }, - /** - * innerText compatibility shim for Firefox - */ toMatchText: function(text) { - var isFirefox = /firefox/i.test(navigator.userAgent); this.message = function() { - return "Expected '" + this.actual.nodeName + "' element to have text '" + text + "'"; + return "Expected '" + (this.actual && this.actual.nodeName) + "' element to have text '" + text + "'"; }; - return this.actual[isFirefox ? 'textContent' : 'innerText'] === text; + return this.actual && this.actual.text && this.actual.text() === text; } }); diff --git a/test/stateDirectivesSpec.js b/test/stateDirectivesSpec.js index 272d9faaa..77638d395 100644 --- a/test/stateDirectivesSpec.js +++ b/test/stateDirectivesSpec.js @@ -196,7 +196,7 @@ describe('uiStateRef', function() { scope.$apply(); $compile(el)(scope); - template = $compile(angular.element(''))(scope); + template = $compile(angular.element('
'))(scope); scope.$digest(); })); diff --git a/test/viewDirectiveSpec.js b/test/viewDirectiveSpec.js index 232e8a5f7..575180a0c 100644 --- a/test/viewDirectiveSpec.js +++ b/test/viewDirectiveSpec.js @@ -7,7 +7,16 @@ describe('uiView', function () { var scope, $compile, elem; beforeEach(function() { - angular.module('ui.router.test', ['ui.router', 'ngAnimate']); + var depends = ['ui.router']; + + try { + angular.module('ngAnimate'); + depends.push('ngAnimate'); + } catch(e) { + angular.module('mock.animate', []).value('$animate', null); + } + + angular.module('ui.router.test', depends); module('ui.router.test'); module('mock.animate'); }); @@ -91,123 +100,143 @@ describe('uiView', function () { })); describe('linking ui-directive', function () { + it('anonymous ui-view should be replaced with the template of the current $state', inject(function ($state, $q, $animate) { - elem.append($compile('
')(scope)); + elem.append($compile('
')(scope)); $state.transitionTo(aState); $q.flush(); - expect($animate.flushNext('leave').element.text()).toBe(''); - expect($animate.flushNext('enter').element.text()).toBe(aState.template); + if ($animate) { + expect($animate.flushNext('leave').element.text()).toBe(''); + expect($animate.flushNext('enter').element.text()).toBe(aState.template); + } })); it('named ui-view should be replaced with the template of the current $state', inject(function ($state, $q, $animate) { - elem.append($compile('
')(scope)); + elem.append($compile('
')(scope)); $state.transitionTo(cState); $q.flush(); - expect($animate.flushNext('leave').element.text()).toBe(''); - expect($animate.flushNext('enter').element.text()).toBe(cState.views.cview.template); + if ($animate) { + expect($animate.flushNext('leave').element.text()).toBe(''); + expect($animate.flushNext('enter').element.text()).toBe(cState.views.cview.template); + } })); it('ui-view should be updated after transition to another state', inject(function ($state, $q, $animate) { - elem.append($compile('
')(scope)); + elem.append($compile('
')(scope)); $state.transitionTo(aState); $q.flush(); - expect($animate.flushNext('leave').element.text()).toBe(''); - expect($animate.flushNext('enter').element.text()).toBe(aState.template); + if ($animate) { + expect($animate.flushNext('leave').element.text()).toBe(''); + expect($animate.flushNext('enter').element.text()).toBe(aState.template); + } $state.transitionTo(bState); $q.flush(); - expect($animate.flushNext('leave').element.text()).toBe(aState.template); - expect($animate.flushNext('enter').element.text()).toBe(bState.template); + if ($animate) { + expect($animate.flushNext('leave').element.text()).toBe(aState.template); + expect($animate.flushNext('enter').element.text()).toBe(bState.template); + } })); it('should handle NOT nested ui-views', inject(function ($state, $q, $animate) { - elem.append($compile('
')(scope)); + elem.append($compile('
')(scope)); $state.transitionTo(dState); $q.flush(); - // expect(elem[0].querySelector('.dview1')).toMatchText(dState.views.dview1.template); - // expect(elem[0].querySelector('.dview2')).toMatchText(dState.views.dview2.template); - - expect($animate.flushNext('leave').element.html()).toBeUndefined(); - expect($animate.flushNext('enter').element.html()).toBe(dState.views.dview1.template); - expect($animate.flushNext('leave').element.html()).toBeUndefined(); - expect($animate.flushNext('enter').element.html()).toBe(dState.views.dview2.template); + if ($animate) { + expect($animate.flushNext('leave').element.html()).toBe(''); + expect($animate.flushNext('enter').element.text()).toBe(dState.views.dview1.template); + expect($animate.flushNext('leave').element.html()).toBe(''); + expect($animate.flushNext('enter').element.text()).toBe(dState.views.dview2.template); + } })); it('should handle nested ui-views (testing two levels deep)', inject(function ($state, $q, $animate) { - elem.append($compile('
')(scope)); + $compile(elem.append('
'))(scope); $state.transitionTo(fState); $q.flush(); - // expect(elem[0].querySelector('.view').querySelector('.eview')).toMatchText(fState.views.eview.template); + if ($animate) { + expect($animate.flushNext('leave').element.text()).toBe(''); + expect($animate.flushNext('enter').element.parent().find('.view')).toMatchText(''); - expect($animate.flushNext('leave').element.text()).toBe(''); - expect($animate.flushNext('enter').element.parent().parent()[0].querySelector('.view').querySelector('.eview')).toMatchText(fState.views.eview.template); + var target = $animate.flushNext('enter').element; + expect(target).toHaveClass('eview'); + expect(target).toMatchText(fState.views.eview.template); + } })); }); describe('handling initial view', function () { it('initial view should be compiled if the view is empty', inject(function ($state, $q, $animate) { var content = 'inner content'; - - elem.append($compile('
')(scope)); + elem.append($compile('
')(scope)); + elem.append($compile('
')(scope)); scope.$apply('content = "' + content + '"'); $state.transitionTo(hState); $q.flush(); - expect($animate.flushNext('leave').element.text()).toBe(''); - expect($animate.flushNext('enter').element.text()).toBe(hState.views.inner.template); + if ($animate) { + expect($animate.flushNext('leave').element.text()).toBe(''); + expect($animate.flushNext('enter').element.text()).toBe(''); + expect($animate.flushNext('enter').element.text()).toBe(hState.views.inner.template); + expect($animate.flushNext('addClass').element.text()).toBe(content); - expect($animate.flushNext('addClass').element.text()).toBe(hState.views.inner.template); - expect($animate.flushNext('addClass').element.text()).toBe(hState.views.inner.template); + // going to the parent state which makes the inner view empty + $state.transitionTo(gState); + $q.flush(); - // going to the parent state which makes the inner view empty - $state.transitionTo(gState); - $q.flush(); + expect($animate.flushNext('leave').element).toMatchText(hState.views.inner.template); - // expect(elem[0].querySelector('.test')).toMatchText(content); - - expect($animate.flushNext('leave').element.text()).toBe(hState.views.inner.template); - expect($animate.flushNext('enter').element.text()).toBe(content); + var target = $animate.flushNext('addClass').element; + expect(target).toHaveClass('test'); + expect(target).toMatchText(content); + } })); // related to issue #435 - it('initial view should be transcluded once to prevent breaking other directives', inject(function ($state, $q) { + it('initial view should be transcluded once to prevent breaking other directives', inject(function ($state, $q, $animate) { scope.items = ["I", "am", "a", "list", "of", "items"]; - elem.append($compile('
')(scope)); + elem.append($compile('
')(scope)); // transition to state that has an initial view $state.transitionTo(iState); $q.flush(); + if ($animate) $animate.flush(); // verify if ng-repeat has been compiled expect(elem.find('li').length).toBe(scope.items.length); @@ -215,12 +244,14 @@ describe('uiView', function () { // transition to another state that replace the initial content $state.transitionTo(jState); $q.flush(); + if ($animate) $animate.flush(); - expect(elem.text()).toBe('jState'); + expect(elem.find('ui-view').find('span').text()).toBe('jState'); // transition back to the state with empty subview and the initial view $state.transitionTo(iState); $q.flush(); + if ($animate) $animate.flush(); // verify if the initial view is correct expect(elem.find('li').length).toBe(scope.items.length); @@ -230,6 +261,8 @@ describe('uiView', function () { scope.items.push(".", "Working?"); }); + if ($animate) $animate.flush(); + // verify if the initial view has been updated expect(elem.find('li').length).toBe(scope.items.length); })); @@ -237,21 +270,21 @@ describe('uiView', function () { describe('autoscroll attribute', function () { it('should autoscroll when unspecified', inject(function ($state, $q, $uiViewScroll) { - elem.append($compile('
')(scope)); + elem.append($compile('
')(scope)); $state.transitionTo(aState); $q.flush(); - expect($uiViewScroll).toHaveBeenCalledWith(elem.find('div')); + expect($uiViewScroll).toHaveBeenCalledWith(elem.find('ui-view')); })); it('should autoscroll when expression is missing', inject(function ($state, $q, $uiViewScroll) { - elem.append($compile('
')(scope)); + elem.append($compile('
')(scope)); $state.transitionTo(aState); $q.flush(); - expect($uiViewScroll).toHaveBeenCalledWith(elem.find('div')); + expect($uiViewScroll).toHaveBeenCalledWith(elem.find('ui-view')); })); - it('should autoscroll based on expression', inject(function ($state, $q, $uiViewScroll) { - elem.append($compile('
')(scope)); + it('should autoscroll based on expression', inject(function ($state, $q, $uiViewScroll, $animate) { + elem.append($compile('
')(scope)); scope.doScroll = false; $state.transitionTo(aState); @@ -261,8 +294,10 @@ describe('uiView', function () { scope.doScroll = true; $state.transitionTo(bState); $q.flush(); - expect($uiViewScroll).toHaveBeenCalledWith(elem.find('div')); + if ($animate) $animate.flush(); + + expect($uiViewScroll).toHaveBeenCalledWith(elem.find('ui-view')); })); }); -}); +}); \ No newline at end of file