From 14b51883c953ae922489b56ac75883506d9b81a3 Mon Sep 17 00:00:00 2001 From: Chirayu Krishnappa Date: Wed, 30 Apr 2014 15:45:32 -0700 Subject: [PATCH] fix(Dirty Checking): fix watching methods/closures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BUG: DynamicFieldGetterFactory::isMethod did not handle methods defined in superclasses. - BUG: Upon detecting a method, the code assumed that you would only invoke it. This broke application code that watched a method (e.g. by way of Component mapping) just to get the closurized value and store it somewhere to invoke later (test case and stack trace at end of this commit message.) - BUG: StaticFieldGetterFactory::method() and DynamicFieldGetterFactory::method() differed.  There was no difference between StaticFieldGetterFactory::method() and StaticFieldGetterFactory::getter().  DynamicFieldGetterFactory::method(), as mentioned before, assumed that the only thing you could do with it was to invoke it (i.e. not a leaf watch.) - There was very little testing for StaticFieldGetterFactory.  This meant that, though it was out of sync with DynamicFieldGetterFactory, no tests were failing. Changes in this commit: - run the same tests against StaticFieldGetterFactory that are run against DynamicFieldGetterFactory - do not call the result of GetterFactory.method() in "set object(value)" - reduce the difference between the two different factories.  GetterFactory now only has one method in it's interface definition - the getter function.  `_MODE_METHOD_INVOKE_`, `isMethod`, `isMethodInvoke`, etc. are gone. **Bug Details:** Refer to the repro case at https://github.com/chirayuk/angular.dart/compare/issue_999%5E...issue_999 ```dart // Given this object. class Foo { bar(x) => x+1; } // This test case (in an appropriate file like `scope_spec.dart`) fails // with a traceback it('should watch closures', (RootScope rootScope, Logger log) { rootScope.context['foo'] = new Foo(); rootScope.context['func'] = null; rootScope.watch('foo.bar', (v, _) { rootScope.context['func'] = v; }); rootScope.watch('func(1)', (v, o) => log([v, o])); rootScope.apply(); expect(log).toEqual([[null, null], [2, null]]); }); ``` **Stack Trace:** Chrome 34.0.1847 (Mac OS X 10.9.2) scope watch/digest should watch closures FAILED Test failed: Caught Closure call with mismatched arguments: function 'DynamicFieldGetterFactory.method.' NoSuchMethodError: incorrect number of arguments passed to method named 'DynamicFieldGetterFactory.method.' Receiver: Closure: (List, Map) => dynamic Tried calling: DynamicFieldGetterFactory.method.(Instance of 'Foo') Found: DynamicFieldGetterFactory.method.(args, namedArgs) #0 Object.noSuchMethod (dart:core-patch/object_patch.dart:45) #1 DirtyCheckingRecord.object= (package:angular/change_detection/dirty_checking_change_detector.dart:465:78) #2 _FieldHandler.acceptValue (package:angular/change_detection/watch_group.dart:630:17) #3 WatchGroup.addFieldWatch (package:angular/change_detection/watch_group.dart:171:29) #4 FieldReadAST.setupWatch (package:angular/change_detection/ast.dart:67:31) #5 WatchGroup.watch. (package:angular/change_detection/watch_group.dart:144:40) #6 _HashMap.putIfAbsent (dart:collection-patch/collection_patch.dart:124) #7 WatchGroup.watch (package:angular/change_detection/watch_group.dart:143:27) #8 Scope.watch (package:angular/core/scope.dart:240:31) #9 main... (/Users/chirayu/work/angular.dart/test/core/scope_spec.dart:1308:24) #10 _LocalInstanceMirror._invoke (dart:mirrors-patch/mirrors_impl.dart:440) #11 _LocalInstanceMirror.invoke (dart:mirrors-patch/mirrors_impl.dart:436) #12 _LocalClosureMirror.apply (dart:mirrors-patch/mirrors_impl.dart:466) #13 DynamicInjector.invoke (package:di/dynamic_injector.dart:97:20) #14 _SpecInjector.inject (package:angular/mock/test_injection.dart:58:22) #15 inject. (package:angular/mock/test_injection.dart:100:44) #16 _rootRun (dart:async/zone.dart:723) #17 _ZoneDelegate.run (dart:async/zone.dart:453) #18 _CustomizedZone.run (dart:async/zone.dart:663) #19 runZoned (dart:async/zone.dart:954) #20 _syncOuter. (package:angular/mock/zone.dart:227:22) #21 _withSetup. (/Users/chirayu/work/angular.dart/test/jasmine_syntax.dart:13:14) #22 _run. (package:unittest/src/test_case.dart:102:27) #23 _rootRunUnary (dart:async/zone.dart:730) #24 _ZoneDelegate.runUnary (dart:async/zone.dart:462) #25 _CustomizedZone.runUnary (dart:async/zone.dart:667) #26 _Future._propagateToListeners.handleValueCallback (dart:async/future_impl.dart:488) #27 _Future._propagateToListeners (dart:async/future_impl.dart:571) #28 _Future._completeWithValue (dart:async/future_impl.dart:331) #29 _Future._asyncComplete. (dart:async/future_impl.dart:393) #30 _rootRun (dart:async/zone.dart:723) #31 _ZoneDelegate.run (dart:async/zone.dart:453) #32 _CustomizedZone.run (dart:async/zone.dart:663) #33 _BaseZone.runGuarded (dart:async/zone.dart:574) #34 _BaseZone.bindCallback. (dart:async/zone.dart:599) #35 _asyncRunCallbackLoop (dart:async/schedule_microtask.dart:23) #36 _asyncRunCallback (dart:async/schedule_microtask.dart:32) #37 _handleMutation (file:///Volumes/data/b/build/slave/dartium-mac-full-dev/build/src/dart/tools/dom/src/native_DOMImplementation.dart:588) DECLARED AT:#0 inject (package:angular/mock/test_injection.dart:97:5) #1 _injectify (/Users/chirayu/work/angular.dart/test/_specs.dart:236:25) #2 iit (/Users/chirayu/work/angular.dart/test/_specs.dart:244:53) #3 main.. (/Users/chirayu/work/angular.dart/test/core/scope_spec.dart:1305:10) #4 describe. (/Users/chirayu/work/angular.dart/test/jasmine_syntax.dart:62:9) #5 group (package:unittest/unittest.dart:396:9) #6 describe (/Users/chirayu/work/angular.dart/test/jasmine_syntax.dart:60:15) #7 describe (/Users/chirayu/work/angular.dart/test/_specs.dart:248:46) #8 main. (/Users/chirayu/work/angular.dart/test/core/scope_spec.dart:1009:13) #9 describe. (/Users/chirayu/work/angular.dart/test/jasmine_syntax.dart:62:9) #10 group (package:unittest/unittest.dart:396:9) #11 describe (/Users/chirayu/work/angular.dart/test/jasmine_syntax.dart:60:15) #12 describe (/Users/chirayu/work/angular.dart/test/_specs.dart:248:46) #13 main (/Users/chirayu/work/angular.dart/test/core/scope_spec.dart:14:11) #14 main. (http://localhost:8765/__adapter_dart_unittest.dart:169:15) #15 _rootRun (dart:async/zone.dart:723) #16 _ZoneDelegate.run (dart:async/zone.dart:453) #17 _CustomizedZone.run (dart:async/zone.dart:663) #18 runZoned (dart:async/zone.dart:954) #19 main (http://localhost:8765/__adapter_dart_unittest.dart:146:11) #0 _SpecInjector.inject (package:angular/mock/test_injection.dart:60:7) #1 inject. (package:angular/mock/test_injection.dart:100:44) #2 _rootRun (dart:async/zone.dart:723) #3 _rootRun (dart:async/zone.dart:724) #4 _rootRun (dart:async/zone.dart:724) #5 _ZoneDelegate.run (dart:async/zone.dart:453) #6 _CustomizedZone.run (dart:async/zone.dart:663) #7 runZoned (dart:async/zone.dart:954) #8 _syncOuter. (package:angular/mock/zone.dart:227:22) #9 _withSetup. (/Users/chirayu/work/angular.dart/test/jasmine_syntax.dart:13:14) #10 _withSetup. (/Users/chirayu/work/angular.dart/test/jasmine_syntax.dart:14:5) #11 _withSetup. (/Users/chirayu/work/angular.dart/test/jasmine_syntax.dart:14:5) #12 _run. (package:unittest/src/test_case.dart:102:27) #13 _rootRunUnary (dart:async/zone.dart:730) #14 _ZoneDelegate.runUnary (dart:async/zone.dart:462) #15 _CustomizedZone.runUnary (dart:async/zone.dart:667) #16 _Future._propagateToListeners.handleValueCallback (dart:async/future_impl.dart:488) #17 _Future._propagateToListeners (dart:async/future_impl.dart:571) #18 _Future._completeWithValue (dart:async/future_impl.dart:331) #19 _Future._asyncComplete. (dart:async/future_impl.dart:393) #20 _rootRun (dart:async/zone.dart:723) #21 _ZoneDelegate.run (dart:async/zone.dart:453) #22 _CustomizedZone.run (dart:async/zone.dart:663) #23 _BaseZone.runGuarded (dart:async/zone.dart:574) #24 _BaseZone.bindCallback. (dart:async/zone.dart:599) #25 _asyncRunCallbackLoop (dart:async/schedule_microtask.dart:23) #26 _asyncRunCallback (dart:async/schedule_microtask.dart:32) #27 _handleMutation (file:///Volumes/data/b/build/slave/dartium-mac-full-dev/build/src/dart/tools/dom/src/native_DOMImplementation.dart:588) Closes #999 --- lib/change_detection/change_detection.dart | 3 - .../dirty_checking_change_detector.dart | 30 +++++-- ...irty_checking_change_detector_dynamic.dart | 19 +---- ...dirty_checking_change_detector_static.dart | 17 ---- lib/change_detection/watch_group.dart | 70 ++++++++-------- .../dirty_checking_change_detector_spec.dart | 82 +++++++++++++++---- test/core/scope_spec.dart | 21 ++++- test/directive/ng_repeat_spec.dart | 10 +++ 8 files changed, 157 insertions(+), 95 deletions(-) diff --git a/lib/change_detection/change_detection.dart b/lib/change_detection/change_detection.dart index 71f1b97dc..5919f1cc2 100644 --- a/lib/change_detection/change_detection.dart +++ b/lib/change_detection/change_detection.dart @@ -174,9 +174,6 @@ typedef dynamic FieldGetter(object); typedef void FieldSetter(object, value); abstract class FieldGetterFactory { - get isMethodInvoke; - bool isMethod(Object object, String name); - Function method(Object object, String name); FieldGetter getter(Object object, String name); } diff --git a/lib/change_detection/dirty_checking_change_detector.dart b/lib/change_detection/dirty_checking_change_detector.dart index 3236122da..e4e1c6844 100644 --- a/lib/change_detection/dirty_checking_change_detector.dart +++ b/lib/change_detection/dirty_checking_change_detector.dart @@ -373,11 +373,13 @@ class DirtyCheckingRecord implements Record, WatchRecord { static const List _MODE_NAMES = const ['MARKER', 'IDENT', 'GETTER', 'MAP[]', 'ITERABLE', 'MAP']; static const int _MODE_MARKER_ = 0; - static const int _MODE_IDENTITY_ = 1; - static const int _MODE_GETTER_ = 2; - static const int _MODE_MAP_FIELD_ = 3; - static const int _MODE_ITERABLE_ = 4; - static const int _MODE_MAP_ = 5; + static const int _MODE_NOOP_ = 1; + static const int _MODE_IDENTITY_ = 2; + static const int _MODE_GETTER_ = 3; + static const int _MODE_GETTER_OR_METHOD_CLOSURE_ = 4; + static const int _MODE_MAP_FIELD_ = 5; + static const int _MODE_ITERABLE_ = 6; + static const int _MODE_MAP_ = 7; final DirtyCheckingChangeDetectorGroup _group; final FieldGetterFactory _fieldGetterFactory; @@ -460,7 +462,7 @@ class DirtyCheckingRecord implements Record, WatchRecord { _mode = _MODE_MAP_FIELD_; _getter = null; } else { - _mode = _MODE_GETTER_; + _mode = _MODE_GETTER_OR_METHOD_CLOSURE_; _getter = _fieldGetterFactory.getter(obj, field); } } @@ -471,14 +473,30 @@ class DirtyCheckingRecord implements Record, WatchRecord { switch (_mode) { case _MODE_MARKER_: return false; + case _MODE_NOOP_: + return false; case _MODE_GETTER_: current = _getter(object); break; + case _MODE_GETTER_OR_METHOD_CLOSURE_: + // NOTE: When Dart looks up a method "foo" on object "x", it returns a + // new closure for each lookup. They compare equal via "==" but are no + // identical(). There's no point getting a new value each time and + // decide it's the same so we'll skip further checking after the first + // time. + current = _getter(object); + if (current is Function && !identical(current, _getter(object))) { + _mode = _MODE_NOOP_; + } else { + _mode = _MODE_GETTER_; + } + break; case _MODE_MAP_FIELD_: current = object[field]; break; case _MODE_IDENTITY_: current = object; + _mode = _MODE_NOOP_; break; case _MODE_MAP_: return (currentValue as _MapChangeRecord)._check(object); diff --git a/lib/change_detection/dirty_checking_change_detector_dynamic.dart b/lib/change_detection/dirty_checking_change_detector_dynamic.dart index fefdfd589..f5f8bb973 100644 --- a/lib/change_detection/dirty_checking_change_detector_dynamic.dart +++ b/lib/change_detection/dirty_checking_change_detector_dynamic.dart @@ -11,26 +11,9 @@ export 'package:angular/change_detection/change_detection.dart' show import 'dart:mirrors'; class DynamicFieldGetterFactory implements FieldGetterFactory { - final isMethodInvoke = true; - - bool isMethod(Object object, String name) { - try { - return method(object, name) != null; - } catch (e, s) { - return false; - } - } - - Function method(Object object, String name) { - Symbol symbol = new Symbol(name); - InstanceMirror instanceMirror = reflect(object); - return (List args, Map namedArgs) => - instanceMirror.invoke(symbol, args, namedArgs).reflectee; - } - FieldGetter getter(Object object, String name) { Symbol symbol = new Symbol(name); InstanceMirror instanceMirror = reflect(object); - return (Object object) => instanceMirror.getField(symbol).reflectee; + return (Object object) => instanceMirror.getField(symbol).reflectee; } } diff --git a/lib/change_detection/dirty_checking_change_detector_static.dart b/lib/change_detection/dirty_checking_change_detector_static.dart index 22a9c76f5..1c7764dfd 100644 --- a/lib/change_detection/dirty_checking_change_detector_static.dart +++ b/lib/change_detection/dirty_checking_change_detector_static.dart @@ -3,30 +3,13 @@ library dirty_checking_change_detector_static; import 'package:angular/change_detection/change_detection.dart'; class StaticFieldGetterFactory implements FieldGetterFactory { - final isMethodInvoke = false; Map getters; StaticFieldGetterFactory(this.getters); - bool isMethod(Object object, String name) { - // We need to know if we are referring to method or field which is a - // function. We can find out by calling it twice and seeing if we get - // the same value. Methods create a new closure each time. - FieldGetter getterFn = getter(object, name); - dynamic property = getterFn(object); - return (property is Function) && - (!identical(property, getterFn(object))); - } - FieldGetter getter(Object object, String name) { var getter = getters[name]; if (getter == null) throw "Missing getter: (o) => o.$name"; return getter; } - - Function method(Object object, String name) { - var method = getters[name]; - if (method == null) throw "Missing method: $name"; - return method(object); - } } diff --git a/lib/change_detection/watch_group.dart b/lib/change_detection/watch_group.dart index b0dbd05f0..f66f4aa4a 100644 --- a/lib/change_detection/watch_group.dart +++ b/lib/change_detection/watch_group.dart @@ -26,8 +26,7 @@ typedef void ChangeLog(String expression, current, previous); * number of arguments with which the function will get called with. */ abstract class FunctionApply { - // dartbug.com/16401 - // dynamic call() { throw new StateError('Use apply()'); } + dynamic call() { throw new StateError('Use apply()'); } dynamic apply(List arguments); } @@ -198,7 +197,7 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { * - [isPure] A pure function is one which holds no internal state. This implies that the * function is idempotent. */ - _EvalWatchRecord addFunctionWatch(/* dartbug.com/16401 Function */ fn, List argsAST, + _EvalWatchRecord addFunctionWatch(Function fn, List argsAST, Map namedArgsAST, String expression, bool isPure) => _addEvalWatch(null, fn, null, argsAST, namedArgsAST, expression, isPure); @@ -214,11 +213,11 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { _EvalWatchRecord addMethodWatch(AST lhs, String name, List argsAST, Map namedArgsAST, String expression) => - _addEvalWatch(lhs, null, name, argsAST, namedArgsAST, expression, false); + _addEvalWatch(lhs, null, name, argsAST, namedArgsAST, expression, false); - _EvalWatchRecord _addEvalWatch(AST lhsAST, /* dartbug.com/16401 Function */ fn, String name, + _EvalWatchRecord _addEvalWatch(AST lhsAST, Function fn, String name, List argsAST, Map namedArgsAST, String expression, bool isPure) { @@ -715,24 +714,24 @@ class _InvokeHandler extends _Handler implements _ArgHandlerList { class _EvalWatchRecord implements WatchRecord<_Handler> { - static const int _MODE_INVALID_ = -2; - static const int _MODE_DELETED_ = -1; - static const int _MODE_MARKER_ = 0; - static const int _MODE_PURE_FUNCTION_ = 1; - static const int _MODE_FUNCTION_ = 2; - static const int _MODE_PURE_FUNCTION_APPLY_ = 3; - static const int _MODE_NULL_ = 4; - static const int _MODE_FIELD_CLOSURE_ = 5; - static const int _MODE_MAP_CLOSURE_ = 6; - static const int _MODE_METHOD_ = 7; - static const int _MODE_METHOD_INVOKE_ = 8; + static const int _MODE_INVALID_ = -2; + static const int _MODE_DELETED_ = -1; + static const int _MODE_MARKER_ = 0; + static const int _MODE_PURE_FUNCTION_ = 1; + static const int _MODE_FUNCTION_ = 2; + static const int _MODE_PURE_FUNCTION_APPLY_ = 3; + static const int _MODE_NULL_ = 4; + static const int _MODE_FIELD_OR_METHOD_CLOSURE_ = 5; + static const int _MODE_METHOD_ = 6; + static const int _MODE_FIELD_CLOSURE_ = 7; + static const int _MODE_MAP_CLOSURE_ = 8; WatchGroup watchGrp; final _Handler handler; final List args; final Map namedArgs = new Map(); final String name; int mode; - /* dartbug.com/16401 Function*/ var fn; + Function fn; FieldGetterFactory _fieldGetterFactory; bool dirtyArgs = true; @@ -789,13 +788,8 @@ class _EvalWatchRecord implements WatchRecord<_Handler> { if (value is Map) { mode = _MODE_MAP_CLOSURE_; } else { - if (_fieldGetterFactory.isMethod(value, name)) { - mode = _fieldGetterFactory.isMethodInvoke ? _MODE_METHOD_INVOKE_ : _MODE_METHOD_; - fn = _fieldGetterFactory.method(value, name); - } else { - mode = _MODE_FIELD_CLOSURE_; - fn = _fieldGetterFactory.getter(value, name); - } + mode = _MODE_FIELD_OR_METHOD_CLOSURE_; + fn = _fieldGetterFactory.getter(value, name); } } } @@ -820,19 +814,31 @@ class _EvalWatchRecord implements WatchRecord<_Handler> { value = (fn as FunctionApply).apply(args); dirtyArgs = false; break; - case _MODE_FIELD_CLOSURE_: + case _MODE_FIELD_OR_METHOD_CLOSURE_: var closure = fn(_object); - value = closure == null ? null : Function.apply(closure, args, namedArgs); - break; - case _MODE_MAP_CLOSURE_: - var closure = object[name]; - value = closure == null ? null : Function.apply(closure, args, namedArgs); + // NOTE: When Dart looks up a method "foo" on object "x", it returns a + // new closure for each lookup. They compare equal via "==" but are no + // identical(). There's no point getting a new value each time and + // decide it's the same so we'll skip further checking after the first + // time. + if (closure is Function && !identical(closure, fn(_object))) { + fn = closure; + mode = _MODE_METHOD_; + } else { + mode = _MODE_FIELD_CLOSURE_; + } + value = (closure == null) ? null : Function.apply(closure, args, namedArgs); break; case _MODE_METHOD_: value = Function.apply(fn, args, namedArgs); break; - case _MODE_METHOD_INVOKE_: - value = fn(args, namedArgs); + case _MODE_FIELD_CLOSURE_: + var closure = fn(_object); + value = (closure == null) ? null : Function.apply(closure, args, namedArgs); + break; + case _MODE_MAP_CLOSURE_: + var closure = object[name]; + value = (closure == null) ? null : Function.apply(closure, args, namedArgs); break; default: assert(false); diff --git a/test/change_detection/dirty_checking_change_detector_spec.dart b/test/change_detection/dirty_checking_change_detector_spec.dart index 3ef529ae4..1107cb28d 100644 --- a/test/change_detection/dirty_checking_change_detector_spec.dart +++ b/test/change_detection/dirty_checking_change_detector_spec.dart @@ -4,31 +4,18 @@ import '../_specs.dart'; import 'package:angular/change_detection/change_detection.dart'; import 'package:angular/change_detection/dirty_checking_change_detector.dart'; import 'package:angular/change_detection/dirty_checking_change_detector_static.dart'; +import 'package:angular/change_detection/dirty_checking_change_detector_dynamic.dart'; import 'dart:collection'; import 'dart:math'; -void main() { - describe('DirtyCheckingChangeDetector', () { +void testWithGetterFactory(FieldGetterFactory getterFactory) { + describe('DirtyCheckingChangeDetector with ${getterFactory.runtimeType}', () { DirtyCheckingChangeDetector detector; - FieldGetterFactory getterFactory = new StaticFieldGetterFactory({ - "first": (o) => o.first, - "age": (o) => o.age, - "last": (o) => o.last, - "toString": (o) => o.toString - }); beforeEach(() { detector = new DirtyCheckingChangeDetector(getterFactory); }); - describe('StaticFieldGetterFactory', () { - it('should detect methods', () { - var obj = new _User(); - expect(getterFactory.isMethod(obj, 'toString')).toEqual(true); - expect(getterFactory.isMethod(obj, 'age')).toEqual(false); - }); - }); - describe('object field', () { it('should detect nothing', () { var changes = detector.collectChanges(); @@ -657,6 +644,42 @@ void main() { }); }); + describe('function watching', () { + it('should detect no changes when watching a function', () { + var user = new _User('marko', 'vuksanovic', 15); + Iterator changeIterator; + + detector..watch(user, 'isUnderAge', null); + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(true); + expect(changeIterator.moveNext()).toEqual(false); + + user.age = 17; + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(false); + + user.age = 30; + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(false); + }); + + it('should detect change when watching a property function', () { + var user = new _User('marko', 'vuksanovic', 30); + Iterator changeIterator; + + detector..watch(user, 'isUnderAgeAsVariable', null); + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(true); + + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(false); + + user.isUnderAgeAsVariable = () => false; + changeIterator = detector.collectChanges(); + expect(changeIterator.moveNext()).toEqual(true); + }); + }); + describe('DuplicateMap', () { DuplicateMap map; beforeEach(() => map = new DuplicateMap()); @@ -689,12 +712,36 @@ void main() { }); } + +void main() { + testWithGetterFactory(new DynamicFieldGetterFactory()); + + testWithGetterFactory(new StaticFieldGetterFactory({ + "first": (o) => o.first, + "age": (o) => o.age, + "last": (o) => o.last, + "toString": (o) => o.toString, + "isUnderAge": (o) => o.isUnderAge, + "isUnderAgeAsVariable": (o) => o.isUnderAgeAsVariable, + })); +} + + class _User { String first; String last; num age; + var isUnderAgeAsVariable; + List list = ['foo', 'bar', 'baz']; + Map map = {'foo': 'bar', 'baz': 'cux'}; + + _User([this.first, this.last, this.age]) { + isUnderAgeAsVariable = isUnderAge; + } - _User([this.first, this.last, this.age]); + bool isUnderAge() { + return age != null ? age < 18 : false; + } } Matcher toEqualCollectionRecord({collection, previous, additions, moves, removals}) => @@ -915,7 +962,6 @@ class MapRecordMatcher extends _CollectionMatcher { } } - class FooBar { static int fooIds = 0; diff --git a/test/core/scope_spec.dart b/test/core/scope_spec.dart index 2fae182ae..4f4e7efbd 100644 --- a/test/core/scope_spec.dart +++ b/test/core/scope_spec.dart @@ -1291,13 +1291,29 @@ void main() { it('should properly watch array of fields 3', (RootScope rootScope, Logger log) { rootScope.context['foo'] = 'abc'; - rootScope.watch('foo.contains("b")', (v, o) => log([v, o])); + String expression = 'foo.contains("b")'; + if (identical(1, 1.0)) { // dart2js + // The previous expression does not work in dart2js (dartbug 13523) + // Use a working one for now. + expression = 'foo.contains("b", 0)'; + } + rootScope.watch(expression, (v, o) => log([v, o])); expect(log).toEqual([]); rootScope.apply(); expect(log).toEqual([[true, null]]); log.clear(); }); + it('should watch closures both as a leaf and as method call', (RootScope rootScope, Logger log) { + rootScope.context['foo'] = new Foo(); + rootScope.context['increment'] = null; + rootScope.watch('foo.increment', (v, _) => rootScope.context['increment'] = v); + rootScope.watch('increment(1)', (v, o) => log([v, o])); + expect(log).toEqual([]); + rootScope.apply(); + expect(log).toEqual([[null, null], [2, null]]); + log.clear(); + }); it('should not trigger new watcher in the flush where it was added', (Scope scope) { var log = [] ; @@ -1623,3 +1639,6 @@ class UnstableList { List get list => new List.generate(3, (i) => i); } +class Foo { + increment(x) => x+1; +} diff --git a/test/directive/ng_repeat_spec.dart b/test/directive/ng_repeat_spec.dart index d0dbd524c..c5d45a978 100644 --- a/test/directive/ng_repeat_spec.dart +++ b/test/directive/ng_repeat_spec.dart @@ -116,6 +116,16 @@ main() { expect(element.querySelectorAll('span').length).toEqual(2); }); + it('should support function as a formatter', () { + scope.context['isEven'] = (num) => num % 2 == 0; + var element = compile( + '
' + '{{r}}' + '
'); + scope.apply(); + expect(element.text).toEqual('2'); + }); + describe('track by', () { it(r'should track using expression function', () {