diff --git a/regression/issue-170.html b/regression/issue-170.html new file mode 100644 index 000000000000..392abf81a87b --- /dev/null +++ b/regression/issue-170.html @@ -0,0 +1,28 @@ + + + + + + + + + {{selection0}} <-- FOO should be shown here + +
+ + + {{selection1}} <-- BAR should be shown here + +
+ + + {{selection2}} <-- 1 should be shown here + + + \ No newline at end of file diff --git a/src/Angular.js b/src/Angular.js index 4d3c360f4a05..9779286804fe 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -53,6 +53,9 @@ function fromCharCode(code) { return String.fromCharCode(code); } var _undefined = undefined, _null = null, $$element = '$element', + $$update = '$update', + $$scope = '$scope', + $$validate = '$validate', $angular = 'angular', $array = 'array', $boolean = 'boolean', @@ -69,6 +72,8 @@ var _undefined = undefined, $number = 'number', $object = 'object', $string = 'string', + $value = 'value', + $selected = 'selected', $undefined = 'undefined', NG_EXCEPTION = 'ng-exception', NG_VALIDATION_ERROR = 'ng-validation-error', diff --git a/src/Compiler.js b/src/Compiler.js index 10d19ea8a22c..a98bd502721c 100644 --- a/src/Compiler.js +++ b/src/Compiler.js @@ -30,6 +30,7 @@ Template.prototype = { if (this.newScope) { childScope = createScope(scope); scope.$onEval(childScope.$eval); + element.data($$scope, childScope); } foreach(this.inits, function(fn) { queue.push(function() { @@ -68,6 +69,17 @@ Template.prototype = { } }; +/* + * Function walks up the element chain looking for the scope associated with the give element. + */ +function retrieveScope(element) { + var scope; + while (element && !(scope = element.data($$scope))) { + element = element.parent(); + } + return scope; +} + /////////////////////////////////// //Compiler ////////////////////////////////// @@ -97,6 +109,7 @@ Compiler.prototype = { element = jqLite(element); var scope = parentScope && parentScope.$eval ? parentScope : createScope(parentScope); + element.data($$scope, scope); return extend(scope, { $element:element, $init: function() { diff --git a/src/Scope.js b/src/Scope.js index 09779453381f..203507a327d0 100644 --- a/src/Scope.js +++ b/src/Scope.js @@ -243,7 +243,6 @@ function createScope(parent, providers, instanceCache) { parent = Parent.prototype = (parent || {}); var instance = new Parent(); var evalLists = {sorted:[]}; - var postList = [], postHash = {}, postId = 0; extend(instance, { 'this': instance, @@ -371,11 +370,6 @@ function createScope(parent, providers, instanceCache) { instance.$tryEval(queue[j].fn, queue[j].handler); } } - while(postList.length) { - fn = postList.shift(); - delete postHash[fn.$postEvalId]; - instance.$tryEval(fn); - } } else if (type === $function) { return exp.call(instance); } else if (type === 'string') { @@ -549,27 +543,6 @@ function createScope(parent, providers, instanceCache) { }); }, - /** - * @workInProgress - * @ngdoc function - * @name angular.scope.$postEval - * @function - */ - $postEval: function(expr) { - if (expr) { - var fn = expressionCompile(expr); - var id = fn.$postEvalId; - if (!id) { - id = '$' + instance.$id + "_" + (postId++); - fn.$postEvalId = id; - } - if (!postHash[id]) { - postList.push(postHash[id] = fn); - } - } - }, - - /** * @workInProgress * @ngdoc function diff --git a/src/directives.js b/src/directives.js index 8a76f17a56ee..d40d6120eacc 100644 --- a/src/directives.js +++ b/src/directives.js @@ -304,7 +304,8 @@ angularDirective("ng:bind-template", function(expression, element){ var REMOVE_ATTRIBUTES = { 'disabled':'disabled', 'readonly':'readOnly', - 'checked':'checked' + 'checked':'checked', + 'selected':'selected' }; /** * @workInProgress @@ -359,27 +360,31 @@ var REMOVE_ATTRIBUTES = { angularDirective("ng:bind-attr", function(expression){ return function(element){ var lastValue = {}; - var updateFn = element.parent().data('$update'); + var updateFn = element.data($$update) || noop; this.$onEval(function(){ - var values = this.$eval(expression); + var values = this.$eval(expression), + dirty = noop; for(var key in values) { var value = compileBindTemplate(values[key]).call(this, element), specialName = REMOVE_ATTRIBUTES[lowercase(key)]; if (lastValue[key] !== value) { lastValue[key] = value; if (specialName) { - if (element[specialName] = toBoolean(value)) { - element.attr(specialName, value); + if (toBoolean(value)) { + element.attr(specialName, specialName); + element.attr('ng-' + specialName, value); } else { - element.removeAttr(key); + element.removeAttr(specialName); + element.removeAttr('ng-' + specialName); } - (element.data('$validate')||noop)(); + (element.data($$validate)||noop)(); } else { element.attr(key, value); } - this.$postEval(updateFn); + dirty = updateFn; } } + dirty(); }, element); }; }); diff --git a/src/validators.js b/src/validators.js index ea35558ebaf7..7318333a728f 100644 --- a/src/validators.js +++ b/src/validators.js @@ -394,7 +394,7 @@ extend(angularValidator, { element.removeClass('ng-input-indicator-wait'); scope.$invalidWidgets.markValid(element); } - element.data('$validate')(); + element.data($$validate)(); scope.$root.$eval(); }); } else if (inputState.inFlight) { diff --git a/src/widgets.js b/src/widgets.js index f34ff05c5296..04353bd5c51e 100644 --- a/src/widgets.js +++ b/src/widgets.js @@ -282,7 +282,7 @@ function valueAccessor(scope, element) { required = requiredExpr === ''; } - element.data('$validate', validate); + element.data($$validate, validate); return { get: function(){ if (lastError) @@ -391,6 +391,7 @@ var textWidget = inputWidget('keyup change', modelAccessor, valueAccessor, initW // 'file': fileWidget??? }; + function initWidgetValue(initValue) { return function (model, view) { var value = view.get(); @@ -461,18 +462,13 @@ function inputWidget(events, modelAccessor, viewAccessor, initFn) { this.$eval(element.attr('ng:init')||''); // Don't register a handler if we are a button (noopAccessor) and there is no action if (action || modelAccessor !== noopAccessor) { - element.bind(events, function(event){ + element.bind(events, function (){ model.set(view.get()); lastValue = model.get(); scope.$tryEval(action, element); scope.$root.$eval(); }); } - function updateView(){ - view.set(lastValue = model.get()); - } - updateView(); - element.data('$update', updateView); scope.$watch(model.get, function(value){ if (lastValue !== value) { view.set(lastValue = value); @@ -494,15 +490,50 @@ angularWidget('select', function(element){ return inputWidgetSelector.call(this, element); }); + +/* + * Consider this: + * + * + * The issue is that the select gets evaluated before option is unrolled. + * This means that the selection is undefined, but the browser + * default behavior is to show the top selection in the list. + * To fix that we register a $update function on the select element + * and the option creation then calls the $update function when it is + * unrolled. The $update function then calls this update function, which + * then tries to determine if the model is unassigned, and if so it tries to + * chose one of the options from the list. + */ angularWidget('option', function(){ this.descend(true); this.directives(true); return function(element) { - this.$postEval(element.parent().data('$update')); + var select = element.parent(); + var scope = retrieveScope(select); + var model = modelFormattedAccessor(scope, select); + var view = valueAccessor(scope, select); + var option = element; + var lastValue = option.attr($value); + var lastSelected = option.attr('ng-' + $selected); + element.data($$update, function(){ + var value = option.attr($value); + var selected = option.attr('ng-' + $selected); + var modelValue = model.get(); + if (lastSelected != selected || lastValue != value) { + lastSelected = selected; + lastValue = value; + if (selected || modelValue == _null || modelValue == _undefined) + model.set(value); + if (value == modelValue) { + view.set(lastValue); + } + } + }); }; }); - /** * @workInProgress * @ngdoc widget diff --git a/test/AngularSpec.js b/test/AngularSpec.js index 6733a7ab835d..f5a202fe2dd5 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -10,6 +10,7 @@ describe('Angular', function(){ scope.$init(); scope.$eval(); expect(onUpdateView).wasCalled(); + dealoc(scope); }); }); diff --git a/test/BinderTest.js b/test/BinderTest.js index d35d46f439bb..58081f25e6e0 100644 --- a/test/BinderTest.js +++ b/test/BinderTest.js @@ -5,6 +5,7 @@ BinderTest.prototype.setUp = function(){ this.compile = function(html, initialScope, parent) { var compiler = new Compiler(angularTextMarkup, angularAttrMarkup, angularDirective, angularWidget); + if (self.element) dealoc(self.element); var element = self.element = jqLite(html); var scope = compiler.compile(element)(element); diff --git a/test/CompilerSpec.js b/test/CompilerSpec.js index fa63ab77dd22..d8c7c1b8d511 100644 --- a/test/CompilerSpec.js +++ b/test/CompilerSpec.js @@ -1,5 +1,5 @@ describe('compiler', function(){ - var compiler, markup, directives, widgets, compile, log; + var compiler, markup, directives, widgets, compile, log, scope; beforeEach(function(){ log = ""; @@ -32,6 +32,10 @@ describe('compiler', function(){ return scope; }; }); + + afterEach(function(){ + dealoc(scope); + }); it('should recognize a directive', function(){ var e = jqLite('
'); @@ -44,7 +48,8 @@ describe('compiler', function(){ }; }; var template = compiler.compile(e); - var init = template(e).$init; + scope = template(e); + var init = scope.$init; expect(log).toEqual("found"); init(); expect(e.hasClass('ng-directive')).toEqual(true); @@ -52,12 +57,12 @@ describe('compiler', function(){ }); it('should recurse to children', function(){ - var scope = compile('
'); + scope = compile('
'); expect(log).toEqual("hello misko"); }); it('should watch scope', function(){ - var scope = compile(''); + scope = compile(''); expect(log).toEqual(""); scope.$eval(); scope.$set('name', 'misko'); @@ -71,7 +76,7 @@ describe('compiler', function(){ it('should prevent descend', function(){ directives.stop = function(){ this.descend(false); }; - var scope = compile(''); + scope = compile(''); expect(log).toEqual("hello misko"); }); @@ -87,7 +92,7 @@ describe('compiler', function(){ }); }; }; - var scope = compile('beforexafter'); + scope = compile('beforexafter'); expect(sortedHtml(scope.$element)).toEqual('
before<#comment>xafter
'); scope.$eval(); expect(sortedHtml(scope.$element)).toEqual('
before<#comment>xxafter
'); @@ -103,7 +108,7 @@ describe('compiler', function(){ textNode[0].nodeValue = 'replaced'; } }); - var scope = compile('beforemiddleafter'); + scope = compile('beforemiddleafter'); expect(sortedHtml(scope.$element[0], true)).toEqual('
beforereplacedafter
'); expect(log).toEqual("hello middle"); }); @@ -116,7 +121,7 @@ describe('compiler', function(){ log += 'init'; }; }; - var scope = compile('push me'); + scope = compile('push me'); expect(lowercase(scope.$element[0].innerHTML)).toEqual('
button
'); expect(log).toEqual('init'); }); @@ -135,7 +140,7 @@ describe('compiler', function(){ if (text == '{{1+2}}') parent.text('3'); }); - var scope = compile('

ignore me

'); + scope = compile('

ignore me

'); expect(scope.$element.text()).toEqual('3'); }); @@ -158,7 +163,7 @@ describe('compiler', function(){ textNode.remove(); } }); - var scope = compile('A---B---C===D'); + scope = compile('A---B---C===D'); expect(sortedHtml(scope.$element)).toEqual('
A
B
C

D
'); }); diff --git a/test/ResourceSpec.js b/test/ResourceSpec.js index e258d5a3c062..a026263bea1f 100644 --- a/test/ResourceSpec.js +++ b/test/ResourceSpec.js @@ -169,6 +169,7 @@ describe("resource", function() { var person = Person.get({id:123}); $browser.xhr.flush(); expect(person.name).toEqual('misko'); + dealoc(scope); }); it('should return the same object when verifying the cache', function(){ @@ -188,6 +189,7 @@ describe("resource", function() { $browser.xhr.flush(); expect(person2Cache).toEqual(person2); expect(person2[0].name).toEqual('rob'); + dealoc(scope); }); describe('failure mode', function(){ diff --git a/test/ScenarioSpec.js b/test/ScenarioSpec.js index 730019a29f69..4a8b5e691647 100644 --- a/test/ScenarioSpec.js +++ b/test/ScenarioSpec.js @@ -1,52 +1,64 @@ describe("ScenarioSpec: Compilation", function(){ - it("should compile dom node and return scope", function(){ - var node = jqLite('
{{b=a+1}}
')[0]; - var scope = compile(node); - scope.$init(); - expect(scope.a).toEqual(1); - expect(scope.b).toEqual(2); + var scope; + + beforeEach(function(){ + scope = null; }); - - it("should compile jQuery node and return scope", function(){ - var scope = compile(jqLite('
{{a=123}}
')).$init(); - expect(jqLite(scope.$element).text()).toEqual('123'); + + afterEach(function(){ + dealoc(scope); }); - - it("should compile text node and return scope", function(){ - var scope = compile('
{{a=123}}
').$init(); - expect(jqLite(scope.$element).text()).toEqual('123'); + + describe('compilation', function(){ + it("should compile dom node and return scope", function(){ + var node = jqLite('
{{b=a+1}}
')[0]; + scope = compile(node); + scope.$init(); + expect(scope.a).toEqual(1); + expect(scope.b).toEqual(2); + }); + + it("should compile jQuery node and return scope", function(){ + scope = compile(jqLite('
{{a=123}}
')).$init(); + expect(jqLite(scope.$element).text()).toEqual('123'); + }); + + it("should compile text node and return scope", function(){ + scope = compile('
{{a=123}}
').$init(); + expect(jqLite(scope.$element).text()).toEqual('123'); + }); }); -}); - -describe("ScenarioSpec: Scope", function(){ - it("should have set, get, eval, $init, updateView methods", function(){ - var scope = compile('
{{a}}
').$init(); - scope.$eval("$invalidWidgets.push({})"); - expect(scope.$set("a", 2)).toEqual(2); - expect(scope.$get("a")).toEqual(2); - expect(scope.$eval("a=3")).toEqual(3); - scope.$eval(); - expect(jqLite(scope.$element).text()).toEqual('3'); + + describe('scope', function(){ + it("should have set, get, eval, $init, updateView methods", function(){ + scope = compile('
{{a}}
').$init(); + scope.$eval("$invalidWidgets.push({})"); + expect(scope.$set("a", 2)).toEqual(2); + expect(scope.$get("a")).toEqual(2); + expect(scope.$eval("a=3")).toEqual(3); + scope.$eval(); + expect(jqLite(scope.$element).text()).toEqual('3'); + }); + + it("should have $ objects", function(){ + scope = compile('
', {$config: {a:"b"}}); + expect(scope.$get('$location')).toBeDefined(); + expect(scope.$get('$eval')).toBeDefined(); + expect(scope.$get('$config')).toBeDefined(); + expect(scope.$get('$config.a')).toEqual("b"); + }); }); - - it("should have $ objects", function(){ - var scope = compile('
', {$config: {a:"b"}}); - expect(scope.$get('$location')).toBeDefined(); - expect(scope.$get('$eval')).toBeDefined(); - expect(scope.$get('$config')).toBeDefined(); - expect(scope.$get('$config.a')).toEqual("b"); + + describe("configuration", function(){ + it("should take location object", function(){ + var url = "http://server/#?book=moby"; + scope = compile("
{{$location}}
"); + var $location = scope.$location; + var $browser = scope.$inject('$browser'); + expect($location.hashSearch.book).toBeUndefined(); + $browser.setUrl(url); + $browser.poll(); + expect($location.hashSearch.book).toEqual('moby'); + }); }); -}); - -describe("ScenarioSpec: configuration", function(){ - it("should take location object", function(){ - var url = "http://server/#?book=moby"; - var scope = compile("
{{$location}}
"); - var $location = scope.$location; - var $browser = scope.$inject('$browser'); - expect($location.hashSearch.book).toBeUndefined(); - $browser.setUrl(url); - $browser.poll(); - expect($location.hashSearch.book).toEqual('moby'); - }); -}); +}); \ No newline at end of file diff --git a/test/ScopeSpec.js b/test/ScopeSpec.js index 38350b17f3bb..acded34bd5b5 100644 --- a/test/ScopeSpec.js +++ b/test/ScopeSpec.js @@ -209,28 +209,6 @@ describe('scope/model', function(){ }); }); - describe('$postEval', function(){ - it('should eval function once and last', function(){ - var log = ''; - var scope = createScope(); - function onceOnly(){log+= '@';} - scope.$onEval(function(){log+= '.';}); - scope.$postEval(function(){log+= '!';}); - scope.$postEval(onceOnly); - scope.$postEval(onceOnly); - scope.$postEval(); // ignore - scope.$eval(); - expect(log).toEqual('.!@'); - scope.$eval(); - expect(log).toEqual('.!@.'); - - scope.$postEval(onceOnly); - scope.$postEval(onceOnly); - scope.$eval(); - expect(log).toEqual('.!@..@'); - }); - }); - describe('$new', function(){ it('should $new should create new child scope and $become controller', function(){ var parent = createScope(null, {exampleService: function(){return 'Example Service';}}); diff --git a/test/ValidatorsTest.js b/test/ValidatorsTest.js index cc77b6da9e4b..f740c7b2ae7e 100644 --- a/test/ValidatorsTest.js +++ b/test/ValidatorsTest.js @@ -104,12 +104,6 @@ describe('Validator:asynchronous', function(){ afterEach(function(){ if (self.$element) self.$element.remove(); - var oldCache = jqCache; - jqCache = {}; - if (size(oldCache)) { - dump(oldCache); - } - expect(size(oldCache)).toEqual(0); }); it('should make a request and show spinner', function(){ diff --git a/test/directivesSpec.js b/test/directivesSpec.js index b70067ba3d5b..ab1813c3cd61 100644 --- a/test/directivesSpec.js +++ b/test/directivesSpec.js @@ -13,8 +13,7 @@ describe("directive", function(){ }); afterEach(function() { - if (model && model.$element) model.$element.remove(); - expect(size(jqCache)).toEqual(0); + dealoc(model); }); it("should ng:init", function() { diff --git a/test/markupSpec.js b/test/markupSpec.js index cb8ee23fbf7e..3234bf2fa160 100644 --- a/test/markupSpec.js +++ b/test/markupSpec.js @@ -14,8 +14,7 @@ describe("markups", function(){ }); afterEach(function(){ - if (element) element.remove(); - expect(size(jqCache)).toEqual(0); + dealoc(element); }); it('should translate {{}} in text', function(){ @@ -63,92 +62,91 @@ describe("markups", function(){ compile(''); expect(sortedHtml(element)).toEqual(''); }); -}); + it('should Parse Text With No Bindings', function(){ + var parts = parseBindings("a"); + assertEquals(parts.length, 1); + assertEquals(parts[0], "a"); + assertTrue(!binding(parts[0])); + }); + + it('should Parse Empty Text', function(){ + var parts = parseBindings(""); + assertEquals(parts.length, 1); + assertEquals(parts[0], ""); + assertTrue(!binding(parts[0])); + }); + + it('should Parse Inner Binding', function(){ + var parts = parseBindings("a{{b}}C"); + assertEquals(parts.length, 3); + assertEquals(parts[0], "a"); + assertTrue(!binding(parts[0])); + assertEquals(parts[1], "{{b}}"); + assertEquals(binding(parts[1]), "b"); + assertEquals(parts[2], "C"); + assertTrue(!binding(parts[2])); + }); + + it('should Parse Ending Binding', function(){ + var parts = parseBindings("a{{b}}"); + assertEquals(parts.length, 2); + assertEquals(parts[0], "a"); + assertTrue(!binding(parts[0])); + assertEquals(parts[1], "{{b}}"); + assertEquals(binding(parts[1]), "b"); + }); + + it('should Parse Begging Binding', function(){ + var parts = parseBindings("{{b}}c"); + assertEquals(parts.length, 2); + assertEquals(parts[0], "{{b}}"); + assertEquals(binding(parts[0]), "b"); + assertEquals(parts[1], "c"); + assertTrue(!binding(parts[1])); + }); + + it('should Parse Loan Binding', function(){ + var parts = parseBindings("{{b}}"); + assertEquals(parts.length, 1); + assertEquals(parts[0], "{{b}}"); + assertEquals(binding(parts[0]), "b"); + }); + + it('should Parse Two Bindings', function(){ + var parts = parseBindings("{{b}}{{c}}"); + assertEquals(parts.length, 2); + assertEquals(parts[0], "{{b}}"); + assertEquals(binding(parts[0]), "b"); + assertEquals(parts[1], "{{c}}"); + assertEquals(binding(parts[1]), "c"); + }); + + it('should Parse Two Bindings With Text In Middle', function(){ + var parts = parseBindings("{{b}}x{{c}}"); + assertEquals(parts.length, 3); + assertEquals(parts[0], "{{b}}"); + assertEquals(binding(parts[0]), "b"); + assertEquals(parts[1], "x"); + assertTrue(!binding(parts[1])); + assertEquals(parts[2], "{{c}}"); + assertEquals(binding(parts[2]), "c"); + }); + + it('should Parse Multiline', function(){ + var parts = parseBindings('"X\nY{{A\nB}}C\nD"'); + assertTrue(!!binding('{{A\nB}}')); + assertEquals(parts.length, 3); + assertEquals(parts[0], '"X\nY'); + assertEquals(parts[1], '{{A\nB}}'); + assertEquals(parts[2], 'C\nD"'); + }); + + it('should Has Binding', function(){ + assertTrue(hasBindings(parseBindings("{{a}}"))); + assertTrue(!hasBindings(parseBindings("a"))); + assertTrue(hasBindings(parseBindings("{{b}}x{{c}}"))); + }); + +}); -var BindingMarkupTest = TestCase("BindingMarkupTest"); - -BindingMarkupTest.prototype.testParseTextWithNoBindings = function(){ - var parts = parseBindings("a"); - assertEquals(parts.length, 1); - assertEquals(parts[0], "a"); - assertTrue(!binding(parts[0])); -}; - -BindingMarkupTest.prototype.testParseEmptyText = function(){ - var parts = parseBindings(""); - assertEquals(parts.length, 1); - assertEquals(parts[0], ""); - assertTrue(!binding(parts[0])); -}; - -BindingMarkupTest.prototype.testParseInnerBinding = function(){ - var parts = parseBindings("a{{b}}c"); - assertEquals(parts.length, 3); - assertEquals(parts[0], "a"); - assertTrue(!binding(parts[0])); - assertEquals(parts[1], "{{b}}"); - assertEquals(binding(parts[1]), "b"); - assertEquals(parts[2], "c"); - assertTrue(!binding(parts[2])); -}; - -BindingMarkupTest.prototype.testParseEndingBinding = function(){ - var parts = parseBindings("a{{b}}"); - assertEquals(parts.length, 2); - assertEquals(parts[0], "a"); - assertTrue(!binding(parts[0])); - assertEquals(parts[1], "{{b}}"); - assertEquals(binding(parts[1]), "b"); -}; - -BindingMarkupTest.prototype.testParseBeggingBinding = function(){ - var parts = parseBindings("{{b}}c"); - assertEquals(parts.length, 2); - assertEquals(parts[0], "{{b}}"); - assertEquals(binding(parts[0]), "b"); - assertEquals(parts[1], "c"); - assertTrue(!binding(parts[1])); -}; - -BindingMarkupTest.prototype.testParseLoanBinding = function(){ - var parts = parseBindings("{{b}}"); - assertEquals(parts.length, 1); - assertEquals(parts[0], "{{b}}"); - assertEquals(binding(parts[0]), "b"); -}; - -BindingMarkupTest.prototype.testParseTwoBindings = function(){ - var parts = parseBindings("{{b}}{{c}}"); - assertEquals(parts.length, 2); - assertEquals(parts[0], "{{b}}"); - assertEquals(binding(parts[0]), "b"); - assertEquals(parts[1], "{{c}}"); - assertEquals(binding(parts[1]), "c"); -}; - -BindingMarkupTest.prototype.testParseTwoBindingsWithTextInMiddle = function(){ - var parts = parseBindings("{{b}}x{{c}}"); - assertEquals(parts.length, 3); - assertEquals(parts[0], "{{b}}"); - assertEquals(binding(parts[0]), "b"); - assertEquals(parts[1], "x"); - assertTrue(!binding(parts[1])); - assertEquals(parts[2], "{{c}}"); - assertEquals(binding(parts[2]), "c"); -}; - -BindingMarkupTest.prototype.testParseMultiline = function(){ - var parts = parseBindings('"X\nY{{A\nB}}C\nD"'); - assertTrue(!!binding('{{A\nB}}')); - assertEquals(parts.length, 3); - assertEquals(parts[0], '"X\nY'); - assertEquals(parts[1], '{{A\nB}}'); - assertEquals(parts[2], 'C\nD"'); -}; - -BindingMarkupTest.prototype.testHasBinding = function(){ - assertTrue(hasBindings(parseBindings("{{a}}"))); - assertTrue(!hasBindings(parseBindings("a"))); - assertTrue(hasBindings(parseBindings("{{b}}x{{c}}"))); -}; diff --git a/test/servicesSpec.js b/test/servicesSpec.js index 13e61b184a0a..ff90e0a18dfc 100644 --- a/test/servicesSpec.js +++ b/test/servicesSpec.js @@ -17,8 +17,7 @@ describe("service", function(){ }); afterEach(function(){ - if (scope && scope.$element) - scope.$element.remove(); + dealoc(scope); }); @@ -202,7 +201,7 @@ describe("service", function(){ }); it('should update hash before any processing', function(){ - var scope = compile('
'); + scope = compile('
'); var log = ''; scope.$watch('$location.hash', function(){ log += this.$location.hashPath + ';'; @@ -259,7 +258,7 @@ describe("service", function(){ describe("$invalidWidgets", function(){ it("should count number of invalid widgets", function(){ - var scope = compile(''); + scope = compile(''); jqLite(document.body).append(scope.$element); scope.$init(); expect(scope.$invalidWidgets.length).toEqual(1); @@ -291,8 +290,8 @@ describe("service", function(){ function BookChapter() { this.log = ''; } - var scope = compile('
').$init(); - var $route = scope.$inject('$route'); + scope = compile('
').$init(); + $route = scope.$inject('$route'); $route.when('/Book/:book/Chapter/:chapter', {controller: BookChapter, template:'Chapter.html'}); $route.when('/Blank'); $route.onChange(function(){ diff --git a/test/testabilityPatch.js b/test/testabilityPatch.js index e8041ac70fda..ea5c8ab71575 100644 --- a/test/testabilityPatch.js +++ b/test/testabilityPatch.js @@ -50,6 +50,25 @@ beforeEach(function(){ }); }); +afterEach(clearJqCache); + +function clearJqCache(){ + var count = 0; + foreachSorted(jqCache, function(value, key){ + count ++; + delete jqCache[key]; + foreach(value, function(value, key){ + if (value.$element) + dump(key, sortedHtml(value.$element)); + else + dump(key, toJson(value)); + }); + }); + if (count) { + fail('Found jqCache references that were not deallocated!'); + } +} + function nakedExpect(obj) { return expect(angular.fromJson(angular.toJson(obj))); } @@ -58,6 +77,11 @@ function childNode(element, index) { return jqLite(element[0].childNodes[index]); } +function dealoc(obj) { + var element = (obj||{}).$element || obj; + if (element && element.dealoc) element.dealoc(); +} + extend(angular, { 'element': jqLite, 'compile': compile, diff --git a/test/widgetsSpec.js b/test/widgetsSpec.js index d3957e667077..cb3b76a16666 100644 --- a/test/widgetsSpec.js +++ b/test/widgetsSpec.js @@ -16,8 +16,7 @@ describe("widget", function(){ }); afterEach(function(){ - if (element && element.dealoc) element.dealoc(); - expect(size(jqCache)).toEqual(0); + dealoc(element); }); describe("input", function(){ @@ -362,7 +361,7 @@ describe("widget", function(){ '' + ''); // childNodes[0] is repeater comment - expect(scope.selection).toEqual(undefined); + expect(scope.selection).toEqual(0); browserTrigger(element[0].childNodes[2], 'change'); expect(scope.selection).toEqual(1); @@ -398,6 +397,32 @@ describe("widget", function(){ scope.$eval(); expect(element[0].childNodes[1].selected).toEqual(true); }); + + it('should select default option on repeater', function(){ + compile( + ''); + expect(scope.selection).toEqual('1'); + }); + + it('should select selected option on repeater', function(){ + compile( + ''); + expect(scope.selection).toEqual('ABC'); + }); + + it('should select dynamically selected option on repeater', function(){ + compile( + ''); + expect(scope.selection).toEqual('2'); + }); + }); it('should support type="select-multiple"', function(){ @@ -476,6 +501,7 @@ describe("widget", function(){ scope.url = '/Book/Moby'; scope.$init(); expect(scope.$element.text()).toEqual('Moby'); + dealoc(scope); }); it("should match sandwich ids", function(){ @@ -491,6 +517,7 @@ describe("widget", function(){ scope.$init(); expect(scope.name).toEqual(undefined); expect(scope.$element.text()).toEqual('works'); + dealoc(scope); }); }); @@ -504,6 +531,7 @@ describe("widget", function(){ scope.$inject('$xhr.cache').data.myUrl = {value:'{{name}}'}; scope.$init(); expect(element.text()).toEqual('misko'); + dealoc(scope); }); it('should remove previously included text if a falsy value is bound to src', function() { @@ -521,6 +549,7 @@ describe("widget", function(){ scope.$eval(); expect(element.text()).toEqual(''); + dealoc(scope); }); it('should allow this for scope', function(){ @@ -532,6 +561,7 @@ describe("widget", function(){ // This should not be 4, but to fix this properly // we need to have real events on the scopes. expect(element.text()).toEqual('4'); + dealoc(scope); }); it('should evaluate onload expression when a partial is loaded', function() { @@ -545,6 +575,7 @@ describe("widget", function(){ scope.$init(); expect(element.text()).toEqual('my partial'); expect(scope.loaded).toBe(true); + dealoc(scope); }); });