diff --git a/lib/scope.dart b/lib/scope.dart
index dbc1c7ac7..6ab2465b6 100644
--- a/lib/scope.dart
+++ b/lib/scope.dart
@@ -282,6 +282,22 @@ class Scope implements Map {
}
+ bool _rootDigestScheduled = false;
+ _scheduleRootDigest() {
+ if ($root._rootDigestScheduled) return;
+ $root._rootDigestScheduled = true;
+ async.runAsync(() {
+ $root._rootDigestScheduled = false;
+ try {
+ $root.$digest();
+ } catch (e, s) {
+ // Sadly, this stack trace is mostly useless now.
+ _exceptionHandler(e, s);
+ throw e;
+ }
+ });
+ }
+
$apply([expr]) {
try {
_beginPhase('\$apply');
@@ -290,12 +306,7 @@ class Scope implements Map {
_exceptionHandler(e, s);
} finally {
_clearPhase();
- try {
- $root.$digest();
- } catch (e, s) {
- _exceptionHandler(e, s);
- throw e;
- }
+ _scheduleRootDigest();
}
}
diff --git a/test/compiler_spec.dart b/test/compiler_spec.dart
index d30f336a9..29a9e323d 100644
--- a/test/compiler_spec.dart
+++ b/test/compiler_spec.dart
@@ -267,39 +267,46 @@ main() {
expect(renderedText(element)).toEqual('inside ');
})));
- it('should create a component with IO', inject(() {
+ it('should create a component with IO', async(inject(() {
var element = $(r'
');
$compile(element)(injector, element);
$rootScope.name = 'misko';
$rootScope.$apply();
+ nextTurn(true);
+
var component = $rootScope.ioComponent;
expect(component.scope.name).toEqual(null);
expect(component.scope.attr).toEqual('A');
expect(component.scope.expr).toEqual('misko');
component.scope.expr = 'angular';
$rootScope.$apply();
+ nextTurn(true);
+
expect($rootScope.name).toEqual('angular');
expect($rootScope.done).toEqual(null);
component.scope.ondone();
expect($rootScope.done).toEqual(true);
- }));
+ })));
- it('should create a component with IO and "=" binding value should be available', inject(() {
+ it('should create a component with IO and "=" binding value should be available', async(inject(() {
$rootScope.name = 'misko';
var element = $(r'
');
$compile(element)(injector, element);
var component = $rootScope.ioComponent;
expect(component.scope.expr).toEqual('misko');
$rootScope.$apply();
+ nextTurn(true);
+
component.scope.expr = 'angular';
$rootScope.$apply();
+ nextTurn(true);
+
expect($rootScope.name).toEqual('angular');
- }));
+ })));
it('should expose mapped attributes as camel case', inject(() {
var element = $(' ');
$compile(element)(injector, element);
- $rootScope.$apply();
var componentScope = $rootScope.camelCase;
expect(componentScope.camelCase).toEqual('6');
}));
@@ -315,8 +322,8 @@ main() {
var element = $(r'');
$compile(element)(injector, element);
$rootScope.$apply();
-
nextTurn(true);
+
expect(element.textWithShadow()).toEqual('WORKED');
})));
@@ -327,12 +334,14 @@ main() {
toBe(PublishTypesAttrDirective._injector.get(PublishTypesDirectiveSuperType));
}));
- it('should allow repeaters over controllers', inject((Logger logger) {
+ it('should allow repeaters over controllers', async(inject((Logger logger) {
var element = $(r' ');
$compile(element)(injector, element);
$rootScope.$apply();
+ nextTurn(true);
+
expect(logger.length).toEqual(2);
- }));
+ })));
});
describe('controller scoping', () {
@@ -343,12 +352,14 @@ main() {
expect(log.result()).toEqual('TabComponent-0; LocalAttrDirective-0; PaneComponent-1; LocalAttrDirective-0; PaneComponent-2; LocalAttrDirective-0');
}));
- it('should reuse controllers for transclusions', inject((Compiler $compile, Scope $rootScope, Log log, Injector injector) {
+ it('should reuse controllers for transclusions', async(inject((Compiler $compile, Scope $rootScope, Log log, Injector injector) {
var element = $('block
');
$compile(element)(injector, element);
$rootScope.$apply();
+ nextTurn(true);
+
expect(log.result()).toEqual('IncludeTransclude; SimpleTransclude');
- }));
+ })));
});
});
diff --git a/test/directives/ng_bind_spec.dart b/test/directives/ng_bind_spec.dart
index c09197964..61c2e8d40 100644
--- a/test/directives/ng_bind_spec.dart
+++ b/test/directives/ng_bind_spec.dart
@@ -16,23 +16,26 @@ main() {
}));
- it('should bind to non string values', inject((Scope scope) {
+ it('should bind to non string values', async(inject((Scope scope) {
var element = _.compile('
');
scope.$apply(() {
scope['value'] = null;
});
+ nextTurn(true);
expect(element.text()).toEqual('');
scope.$apply(() {
scope['value'] = true;
});
+ nextTurn(true);
expect(element.text()).toEqual('true');
scope.$apply(() {
scope['value'] = 1;
});
+ nextTurn(true);
expect(element.text()).toEqual('1');
- }));
+ })));
});
}
diff --git a/test/directives/ng_class_spec.dart b/test/directives/ng_class_spec.dart
index 5b925d643..c52dac668 100644
--- a/test/directives/ng_class_spec.dart
+++ b/test/directives/ng_class_spec.dart
@@ -17,23 +17,27 @@ main() {
}));
- it('should add and remove class dynamically', () {
+ it('should add and remove class dynamically', async(() {
compile('
');
rootScope.$apply(() {
rootScope['active'] = 'active';
});
+ nextTurn(true);
+
expect(element.hasClass('active')).toBe(true);
rootScope.$apply(() {
rootScope['active'] = 'inactive';
});
+ nextTurn(true);
+
expect(element.hasClass('active')).toBe(false);
expect(element.hasClass('inactive')).toBe(true);
- });
+ }));
- it('should preserve originally defined classes', () {
+ it('should preserve originally defined classes', async(() {
compile('
');
expect(element.hasClass('original')).toBe(true);
@@ -41,11 +45,13 @@ main() {
rootScope.$apply(() {
rootScope['active'] = 'something';
});
+ nextTurn(true);
+
expect(element.hasClass('original')).toBe(true);
- });
+ }));
- it('should preserve classes that has been added after compilation', () {
+ it('should preserve classes that has been added after compilation', async(() {
compile('
');
element[0].classes.add('after-compile');
@@ -54,16 +60,20 @@ main() {
rootScope.$apply(() {
rootScope['active'] = 'something';
});
+ nextTurn(true);
+
expect(element.hasClass('after-compile')).toBe(true);
- });
+ }));
- it('should allow multiple classes separated by a space', () {
+ it('should allow multiple classes separated by a space', async(() {
compile('
');
rootScope.$apply(() {
rootScope['active'] = 'first second';
});
+ nextTurn(true);
+
expect(element).toHaveClass('first');
expect(element).toHaveClass('second');
expect(element).toHaveClass('original');
@@ -71,17 +81,21 @@ main() {
rootScope.$apply(() {
rootScope['active'] = 'third first';
});
+ nextTurn(true);
+
expect(element).toHaveClass('first');
expect(element).not.toHaveClass('second');
expect(element).toHaveClass('third');
expect(element).toHaveClass('original');
- });
+ }));
- it('should update value that was set before compilation', () {
+ it('should update value that was set before compilation', async(() {
rootScope['active'] = 'something';
compile('
');
+ nextTurn(true);
+
expect(element).toHaveClass('something');
- });
+ }));
});
}
diff --git a/test/directives/ng_controller_spec.dart b/test/directives/ng_controller_spec.dart
index 8d59cd5a0..59f27d1ad 100644
--- a/test/directives/ng_controller_spec.dart
+++ b/test/directives/ng_controller_spec.dart
@@ -25,29 +25,30 @@ main() {
rootScope = scope;
compiler(element)(injector, element);
scope.$apply(applyFn);
+ nextTurn(true);
};
}));
- it('should instantiate controller', () {
+ it('should instantiate controller', async(() {
compile('');
expect(element.find('.controller').text()).toEqual('Hi Vojta');
- });
+ }));
- it('should create a new scope', () {
+ it('should create a new scope', async(() {
compile('', () {
rootScope['name'] = 'parent';
});
expect(element.find('.controller').text()).toEqual('Hi Vojta');
expect(element.find('.siblink').text()).toEqual('parent');
- });
+ }));
- it('should export controller', () {
+ it('should export controller', async(() {
compile('');
expect(element.find('.controller').text()).toEqual('Hi name on controller');
- });
+ }));
});
}
diff --git a/test/directives/ng_disabled_spec.dart b/test/directives/ng_disabled_spec.dart
index 3e785fb1c..5d0d398d1 100644
--- a/test/directives/ng_disabled_spec.dart
+++ b/test/directives/ng_disabled_spec.dart
@@ -8,37 +8,47 @@ describe('ng-disabled', () {
beforeEach(beforeEachTestBed((tb) => _ = tb));
- it('should disable/enable the element based on the model', inject((Scope scope) {
+ it('should disable/enable the element based on the model', async(inject((Scope scope) {
var element = _.compile('x ');
scope.$apply(() {
scope['isDisabled'] = true;
});
+ nextTurn(true);
+
expect(element[0].disabled).toBe(true);
scope.$apply(() {
scope['isDisabled'] = false;
});
+ nextTurn(true);
+
expect(element[0].disabled).toBe(false);
- }));
+ })));
- it('should accept non boolean values', inject((Scope scope) {
+ it('should accept non boolean values', async(inject((Scope scope) {
var element = _.compile('x ');
scope.$apply(() {
scope['isDisabled'] = null;
});
+ nextTurn(true);
+
expect(element[0].disabled).toBe(false);
scope.$apply(() {
scope['isDisabled'] = 1;
});
+ nextTurn(true);
+
expect(element[0].disabled).toBe(true);
scope.$apply(() {
scope['isDisabled'] = 0;
});
+ nextTurn(true);
+
expect(element[0].disabled).toBe(false);
- }));
+ })));
});
diff --git a/test/directives/ng_hide_spec.dart b/test/directives/ng_hide_spec.dart
index a8c084f66..09b253ac2 100644
--- a/test/directives/ng_hide_spec.dart
+++ b/test/directives/ng_hide_spec.dart
@@ -12,11 +12,12 @@ main() {
rootScope = scope;
compiler(element)(injector, element);
scope.$apply(applyFn);
+ nextTurn(true);
};
}));
- it('should add/remove ng-hide class', () {
+ it('should add/remove ng-hide class', async(() {
compile('
');
expect(element).not.toHaveClass('ng-hide');
@@ -24,12 +25,16 @@ main() {
rootScope.$apply(() {
rootScope['isHidden'] = true;
});
+ nextTurn(true);
+
expect(element).toHaveClass('ng-hide');
rootScope.$apply(() {
rootScope['isHidden'] = false;
});
+ nextTurn(true);
+
expect(element).not.toHaveClass('ng-hide');
- });
+ }));
});
}
diff --git a/test/directives/ng_if_spec.dart b/test/directives/ng_if_spec.dart
index ed322d219..af0050b9f 100644
--- a/test/directives/ng_if_spec.dart
+++ b/test/directives/ng_if_spec.dart
@@ -18,10 +18,11 @@ main() {
rootScope = scope;
compiler(element)(injector, element);
scope.$apply(applyFn);
+ nextTurn(true);
};
}));
- it('should add/remove the element', () {
+ it('should add/remove the element', async(() {
compile('content
');
expect(element.find('span').html()).toEqual('');
@@ -29,23 +30,29 @@ main() {
rootScope.$apply(() {
rootScope['isVisible'] = true;
});
+ nextTurn(true);
+
expect(element.find('span').html()).toEqual('content');
rootScope.$apply(() {
rootScope['isVisible'] = false;
});
+ nextTurn(true);
+
expect(element.find('span').html()).toEqual('');
- });
+ }));
- it('should not cause ng-click to throw an exception', () {
+ it('should not cause ng-click to throw an exception', async(() {
compile('content
');
rootScope.$apply(() {
rootScope['isVisible'] = false;
});
+ nextTurn(true);
+
expect(element.find('span').html()).toEqual('');
- });
+ }));
- it('should prevent other directives from running when disabled', inject((Log log) {
+ it('should prevent other directives from running when disabled', async(inject((Log log) {
compile('
content ');
expect(element.find('span').html()).toEqual('');
@@ -53,6 +60,8 @@ main() {
rootScope.$apply(() {
rootScope['isVisible'] = false;
});
+ nextTurn(true);
+
expect(element.find('span').html()).toEqual('');
expect(log.result()).toEqual('ALWAYS');
@@ -60,9 +69,10 @@ main() {
rootScope.$apply(() {
rootScope['isVisible'] = true;
});
+ nextTurn(true);
+
expect(element.find('span').html()).toEqual('content');
expect(log.result()).toEqual('ALWAYS; JAMES');
-
- }));
+ })));
});
}
diff --git a/test/directives/ng_include_spec.dart b/test/directives/ng_include_spec.dart
index ae46c9aca..a26a4b66c 100644
--- a/test/directives/ng_include_spec.dart
+++ b/test/directives/ng_include_spec.dart
@@ -19,20 +19,22 @@ main() {
scope['name'] = 'Vojta';
scope['template'] = 'tpl.html';
});
+ nextTurn(true);
- nextTurn(); // load the template from cache.
expect(element.text()).toEqual('my name is Vojta');
})));
- it('should support inlined templates', inject((Scope scope) {
+ it('should support inlined templates', async(inject((Scope scope) {
var element = _.compile('
');
scope.$apply(() {
scope['name'] = 'Vojta';
scope['template'] = 'my inlined name is {{name}} ';
});
+ nextTurn(true);
+
expect(element.text()).toEqual('my inlined name is Vojta');
- }));
+ })));
});
}
diff --git a/test/directives/ng_model_spec.dart b/test/directives/ng_model_spec.dart
index d64fa28bf..88e84093d 100644
--- a/test/directives/ng_model_spec.dart
+++ b/test/directives/ng_model_spec.dart
@@ -8,15 +8,17 @@ describe('ng-model', () {
beforeEach(beforeEachTestBed((tb) => _ = tb));
describe('type="text"', () {
- it('should update input value from model', inject(() {
+ it('should update input value from model', async(inject(() {
_.compile(' ');
_.rootScope.$digest();
expect(_.rootElement.prop('value')).toEqual('');
_.rootScope.$apply('model = "misko"');
+ nextTurn(true);
+
expect(_.rootElement.prop('value')).toEqual('misko');
- }));
+ })));
it('should update model from the input value', inject(() {
_.compile(' ');
@@ -32,39 +34,49 @@ describe('ng-model', () {
describe('type="checkbox"', () {
- it('should update input value from model', inject((Scope scope) {
+ it('should update input value from model', async(inject((Scope scope) {
var element = _.compile(' ');
scope.$apply(() {
scope['model'] = true;
});
+ nextTurn(true);
+
expect(element[0].checked).toBe(true);
scope.$apply(() {
scope['model'] = false;
});
+ nextTurn(true);
+
expect(element[0].checked).toBe(false);
- }));
+ })));
- it('should allow non boolean values like null, 0, 1', inject((Scope scope) {
+ it('should allow non boolean values like null, 0, 1', async(inject((Scope scope) {
var element = _.compile(' ');
scope.$apply(() {
scope['model'] = 0;
});
+ nextTurn(true);
+
expect(element[0].checked).toBe(false);
scope.$apply(() {
scope['model'] = 1;
});
+ nextTurn(true);
+
expect(element[0].checked).toBe(true);
scope.$apply(() {
scope['model'] = null;
});
+ nextTurn(true);
+
expect(element[0].checked).toBe(false);
- }));
+ })));
it('should update model from the input value', inject((Scope scope) {
diff --git a/test/directives/ng_repeat_spec.dart b/test/directives/ng_repeat_spec.dart
index a52302a09..483bee19a 100644
--- a/test/directives/ng_repeat_spec.dart
+++ b/test/directives/ng_repeat_spec.dart
@@ -20,7 +20,7 @@ main() {
BlockFactory blockFactory = compiler(element);
Block block = blockFactory(injector, element);
scope.items = ['a', 'b'];
- scope.$apply();
+ scope.$digest();
expect(element.text()).toEqual('ab');
}));
diff --git a/test/directives/ng_show_spec.dart b/test/directives/ng_show_spec.dart
index 45c638e81..26526aee2 100644
--- a/test/directives/ng_show_spec.dart
+++ b/test/directives/ng_show_spec.dart
@@ -12,11 +12,12 @@ main() {
rootScope = scope;
compiler(element)(injector, element);
scope.$apply(applyFn);
+ nextTurn(true);
};
}));
- it('should add/remove ng-show class', () {
+ it('should add/remove ng-show class', async(() {
compile('
');
expect(element).not.toHaveClass('ng-show');
@@ -24,15 +25,19 @@ main() {
rootScope.$apply(() {
rootScope['isVisible'] = true;
});
+ nextTurn(true);
+
expect(element).toHaveClass('ng-show');
rootScope.$apply(() {
rootScope['isVisible'] = false;
});
+ nextTurn(true);
+
expect(element).not.toHaveClass('ng-show');
- });
+ }));
- it('should work together with ng-class', () {
+ it('should work together with ng-class', async(() {
compile('
');
expect(element).not.toHaveClass('active');
@@ -41,14 +46,18 @@ main() {
rootScope.$apply(() {
rootScope['currentCls'] = 'active';
});
+ nextTurn(true);
+
expect(element).toHaveClass('active');
expect(element).not.toHaveClass('ng-show');
rootScope.$apply(() {
rootScope['isVisible'] = true;
});
+ nextTurn(true);
+
expect(element).toHaveClass('active');
expect(element).toHaveClass('ng-show');
- });
+ }));
});
}
diff --git a/test/scope_spec.dart b/test/scope_spec.dart
index 8f965a960..d4e32e44e 100644
--- a/test/scope_spec.dart
+++ b/test/scope_spec.dart
@@ -436,16 +436,18 @@ main() {
describe(r'$apply', () {
- it(r'should apply expression with full lifecycle', inject((Scope $rootScope) {
+ it(r'should apply expression with full lifecycle', async(inject((Scope $rootScope) {
var log = '';
var child = $rootScope.$new();
$rootScope.$watch('a', (a, _, __) { log += '1'; });
child.$apply(r'$parent.a=0');
+ nextTurn(true);
+
expect(log).toEqual('1');
- }));
+ })));
- it(r'should catch exceptions', () {
+ it(r'should catch exceptions', async(() {
module((Module module) => module.type(ExceptionHandler, LogExceptionHandler));
inject((Scope $rootScope, ExceptionHandler $exceptionHandler) {
var log = [];
@@ -453,12 +455,14 @@ main() {
$rootScope.$watch('a', (a, _, __) => log.add('1'));
$rootScope.a = 0;
child.$apply((_, __) { throw 'MyError'; });
+ nextTurn(true);
+
expect(log.join(',')).toEqual('1');
expect($exceptionHandler.errors[0].error).toEqual('MyError');
$exceptionHandler.errors.removeAt(0);
$exceptionHandler.assertEmpty();
});
- });
+ }));
it(r'should log exceptions from $digest', () {
@@ -471,9 +475,10 @@ main() {
$rootScope.$watch('b', (_, __, ___) {$rootScope.a++;});
$rootScope.a = $rootScope.b = 0;
- expect(() {
+ expect(async(() {
$rootScope.$apply();
- }).toThrow('2 \$digest() iterations reached. Aborting!');
+ nextTurn(true);
+ })).toThrow('2 \$digest() iterations reached. Aborting!');
expect($exceptionHandler.errors[0].error).toContain('2 \$digest() iterations reached.');
$exceptionHandler.errors.removeAt(0);
@@ -482,6 +487,17 @@ main() {
});
});
+ it(r'should run $digest a minimum number of times', async(inject((Scope scope) {
+ var a = 0;
+ // This watch value getter is executed twice during a digest.
+ scope.$watch(() => a++ == -4);
+
+ scope.$apply();
+ scope.$apply();
+ nextTurn(true);
+
+ expect(a).toEqual(2);
+ })));
describe(r'exceptions', () {
var log;
@@ -496,21 +512,25 @@ main() {
}));
- it(r'should execute and return value and update', inject(
+ it(r'should execute and return value and update', async(inject(
(Scope $rootScope, ExceptionHandler $exceptionHandler) {
$rootScope.name = 'abc';
expect($rootScope.$apply((scope) => scope.name)).toEqual('abc');
+ nextTurn(true);
+
expect(log).toEqual(r'$digest;');
$exceptionHandler.assertEmpty();
- }));
+ })));
- it(r'should catch exception and update', inject((Scope $rootScope, ExceptionHandler $exceptionHandler) {
+ it(r'should catch exception and update', async(inject((Scope $rootScope, ExceptionHandler $exceptionHandler) {
var error = 'MyError';
$rootScope.$apply(() { throw error; });
+ nextTurn(true);
+
expect(log).toEqual(r'$digest;');
expect($exceptionHandler.errors[0].error).toEqual(error);
- }));
+ })));
});
@@ -527,13 +547,14 @@ main() {
it(r'should throw an exception if $apply is called while flushing evalAsync queue', inject(
(Scope $rootScope) {
- expect(() {
+ expect(async(() {
$rootScope.$apply(() {
$rootScope.$evalAsync(() {
$rootScope.$apply();
});
});
- }).toThrow(r'$digest already in progress');
+ nextTurn(true);
+ })).toThrow(r'$digest already in progress');
}));
@@ -543,24 +564,39 @@ main() {
childScope1.$watch('x', () {
childScope1.$apply();
});
- expect(() { childScope1.$apply(); }).toThrow(r'$digest already in progress');
+ expect(async(() {
+ childScope1.$apply();
+ nextTurn(true);
+ })).toThrow(r'$digest already in progress');
}));
it(r'should thrown an exception if $apply in called from a watch fn (after init)', inject(
(Scope $rootScope) {
+ // async calls in this test. Since async() traps exceptions, we need
+ // to embed an async *inside* the expect().toThrow() call instead
+ // of wrapping the entire function in an async().
+ //
+ // However, the first $apply() need to resolve before the second
+ // $apply. Thus, we have two async() calls.
var childScope2 = $rootScope.$new();
- childScope2.$apply(() {
- childScope2.$watch('x', (newVal, oldVal) {
- if (newVal != oldVal) {
- childScope2.$apply();
- }
+ async(() {
+ childScope2.$apply(() {
+ childScope2.$watch('x', (newVal, oldVal) {
+ if (newVal != oldVal) {
+ childScope2.$apply();
+ }
+ });
});
- });
+ nextTurn(true);
+ })();
- expect(() { childScope2.$apply(() {
- childScope2.x = 'something';
- }); }).toThrow(r'$digest already in progress');
+ expect(async(() {
+ childScope2.$apply(() {
+ childScope2.x = 'something';
+ });
+ nextTurn(true);
+ })).toThrow(r'$digest already in progress');
}));
});
});