Skip to content
This repository was archived by the owner on Feb 22, 2018. It is now read-only.

Commit 14b5188

Browse files
chirayukjbdeboer
authored andcommitted
fix(Dirty Checking): fix watching methods/closures
- 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 chirayuk/angular.dart@issue_999^...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.<anonymous closure>' NoSuchMethodError: incorrect number of arguments passed to method named 'DynamicFieldGetterFactory.method.<anonymous closure>' Receiver: Closure: (List, Map) => dynamic Tried calling: DynamicFieldGetterFactory.method.<anonymous closure>(Instance of 'Foo') Found: DynamicFieldGetterFactory.method.<anonymous closure>(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.<anonymous closure> (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.<anonymous closure>.<anonymous closure>.<anonymous closure> (/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.<anonymous closure> (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.<anonymous closure> (package:angular/mock/zone.dart:227:22) #21 _withSetup.<anonymous closure> (/Users/chirayu/work/angular.dart/test/jasmine_syntax.dart:13:14) #22 _run.<anonymous closure> (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.<anonymous closure> (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.<anonymous closure> (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.<anonymous closure>.<anonymous closure> (/Users/chirayu/work/angular.dart/test/core/scope_spec.dart:1305:10) #4 describe.<anonymous closure> (/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.<anonymous closure> (/Users/chirayu/work/angular.dart/test/core/scope_spec.dart:1009:13) #9 describe.<anonymous closure> (/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.<anonymous closure> (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.<anonymous closure> (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.<anonymous closure> (package:angular/mock/zone.dart:227:22) #9 _withSetup.<anonymous closure> (/Users/chirayu/work/angular.dart/test/jasmine_syntax.dart:13:14) #10 _withSetup.<anonymous closure> (/Users/chirayu/work/angular.dart/test/jasmine_syntax.dart:14:5) #11 _withSetup.<anonymous closure> (/Users/chirayu/work/angular.dart/test/jasmine_syntax.dart:14:5) #12 _run.<anonymous closure> (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.<anonymous closure> (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.<anonymous closure> (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
1 parent 1898fed commit 14b5188

8 files changed

+157
-95
lines changed

lib/change_detection/change_detection.dart

-3
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,6 @@ typedef dynamic FieldGetter(object);
174174
typedef void FieldSetter(object, value);
175175

176176
abstract class FieldGetterFactory {
177-
get isMethodInvoke;
178-
bool isMethod(Object object, String name);
179-
Function method(Object object, String name);
180177
FieldGetter getter(Object object, String name);
181178
}
182179

lib/change_detection/dirty_checking_change_detector.dart

+24-6
Original file line numberDiff line numberDiff line change
@@ -373,11 +373,13 @@ class DirtyCheckingRecord<H> implements Record<H>, WatchRecord<H> {
373373
static const List<String> _MODE_NAMES =
374374
const ['MARKER', 'IDENT', 'GETTER', 'MAP[]', 'ITERABLE', 'MAP'];
375375
static const int _MODE_MARKER_ = 0;
376-
static const int _MODE_IDENTITY_ = 1;
377-
static const int _MODE_GETTER_ = 2;
378-
static const int _MODE_MAP_FIELD_ = 3;
379-
static const int _MODE_ITERABLE_ = 4;
380-
static const int _MODE_MAP_ = 5;
376+
static const int _MODE_NOOP_ = 1;
377+
static const int _MODE_IDENTITY_ = 2;
378+
static const int _MODE_GETTER_ = 3;
379+
static const int _MODE_GETTER_OR_METHOD_CLOSURE_ = 4;
380+
static const int _MODE_MAP_FIELD_ = 5;
381+
static const int _MODE_ITERABLE_ = 6;
382+
static const int _MODE_MAP_ = 7;
381383

382384
final DirtyCheckingChangeDetectorGroup _group;
383385
final FieldGetterFactory _fieldGetterFactory;
@@ -460,7 +462,7 @@ class DirtyCheckingRecord<H> implements Record<H>, WatchRecord<H> {
460462
_mode = _MODE_MAP_FIELD_;
461463
_getter = null;
462464
} else {
463-
_mode = _MODE_GETTER_;
465+
_mode = _MODE_GETTER_OR_METHOD_CLOSURE_;
464466
_getter = _fieldGetterFactory.getter(obj, field);
465467
}
466468
}
@@ -471,14 +473,30 @@ class DirtyCheckingRecord<H> implements Record<H>, WatchRecord<H> {
471473
switch (_mode) {
472474
case _MODE_MARKER_:
473475
return false;
476+
case _MODE_NOOP_:
477+
return false;
474478
case _MODE_GETTER_:
475479
current = _getter(object);
476480
break;
481+
case _MODE_GETTER_OR_METHOD_CLOSURE_:
482+
// NOTE: When Dart looks up a method "foo" on object "x", it returns a
483+
// new closure for each lookup. They compare equal via "==" but are no
484+
// identical(). There's no point getting a new value each time and
485+
// decide it's the same so we'll skip further checking after the first
486+
// time.
487+
current = _getter(object);
488+
if (current is Function && !identical(current, _getter(object))) {
489+
_mode = _MODE_NOOP_;
490+
} else {
491+
_mode = _MODE_GETTER_;
492+
}
493+
break;
477494
case _MODE_MAP_FIELD_:
478495
current = object[field];
479496
break;
480497
case _MODE_IDENTITY_:
481498
current = object;
499+
_mode = _MODE_NOOP_;
482500
break;
483501
case _MODE_MAP_:
484502
return (currentValue as _MapChangeRecord)._check(object);

lib/change_detection/dirty_checking_change_detector_dynamic.dart

+1-18
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,9 @@ export 'package:angular/change_detection/change_detection.dart' show
1111
import 'dart:mirrors';
1212

1313
class DynamicFieldGetterFactory implements FieldGetterFactory {
14-
final isMethodInvoke = true;
15-
16-
bool isMethod(Object object, String name) {
17-
try {
18-
return method(object, name) != null;
19-
} catch (e, s) {
20-
return false;
21-
}
22-
}
23-
24-
Function method(Object object, String name) {
25-
Symbol symbol = new Symbol(name);
26-
InstanceMirror instanceMirror = reflect(object);
27-
return (List args, Map namedArgs) =>
28-
instanceMirror.invoke(symbol, args, namedArgs).reflectee;
29-
}
30-
3114
FieldGetter getter(Object object, String name) {
3215
Symbol symbol = new Symbol(name);
3316
InstanceMirror instanceMirror = reflect(object);
34-
return (Object object) => instanceMirror.getField(symbol).reflectee;
17+
return (Object object) => instanceMirror.getField(symbol).reflectee;
3518
}
3619
}

lib/change_detection/dirty_checking_change_detector_static.dart

-17
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,13 @@ library dirty_checking_change_detector_static;
33
import 'package:angular/change_detection/change_detection.dart';
44

55
class StaticFieldGetterFactory implements FieldGetterFactory {
6-
final isMethodInvoke = false;
76
Map<String, FieldGetter> getters;
87

98
StaticFieldGetterFactory(this.getters);
109

11-
bool isMethod(Object object, String name) {
12-
// We need to know if we are referring to method or field which is a
13-
// function. We can find out by calling it twice and seeing if we get
14-
// the same value. Methods create a new closure each time.
15-
FieldGetter getterFn = getter(object, name);
16-
dynamic property = getterFn(object);
17-
return (property is Function) &&
18-
(!identical(property, getterFn(object)));
19-
}
20-
2110
FieldGetter getter(Object object, String name) {
2211
var getter = getters[name];
2312
if (getter == null) throw "Missing getter: (o) => o.$name";
2413
return getter;
2514
}
26-
27-
Function method(Object object, String name) {
28-
var method = getters[name];
29-
if (method == null) throw "Missing method: $name";
30-
return method(object);
31-
}
3215
}

lib/change_detection/watch_group.dart

+38-32
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ typedef void ChangeLog(String expression, current, previous);
2626
* number of arguments with which the function will get called with.
2727
*/
2828
abstract class FunctionApply {
29-
// dartbug.com/16401
30-
// dynamic call() { throw new StateError('Use apply()'); }
29+
dynamic call() { throw new StateError('Use apply()'); }
3130
dynamic apply(List arguments);
3231
}
3332

@@ -198,7 +197,7 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList {
198197
* - [isPure] A pure function is one which holds no internal state. This implies that the
199198
* function is idempotent.
200199
*/
201-
_EvalWatchRecord addFunctionWatch(/* dartbug.com/16401 Function */ fn, List<AST> argsAST,
200+
_EvalWatchRecord addFunctionWatch(Function fn, List<AST> argsAST,
202201
Map<Symbol, AST> namedArgsAST,
203202
String expression, bool isPure) =>
204203
_addEvalWatch(null, fn, null, argsAST, namedArgsAST, expression, isPure);
@@ -214,11 +213,11 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList {
214213
_EvalWatchRecord addMethodWatch(AST lhs, String name, List<AST> argsAST,
215214
Map<Symbol, AST> namedArgsAST,
216215
String expression) =>
217-
_addEvalWatch(lhs, null, name, argsAST, namedArgsAST, expression, false);
216+
_addEvalWatch(lhs, null, name, argsAST, namedArgsAST, expression, false);
218217

219218

220219

221-
_EvalWatchRecord _addEvalWatch(AST lhsAST, /* dartbug.com/16401 Function */ fn, String name,
220+
_EvalWatchRecord _addEvalWatch(AST lhsAST, Function fn, String name,
222221
List<AST> argsAST,
223222
Map<Symbol, AST> namedArgsAST,
224223
String expression, bool isPure) {
@@ -715,24 +714,24 @@ class _InvokeHandler extends _Handler implements _ArgHandlerList {
715714

716715

717716
class _EvalWatchRecord implements WatchRecord<_Handler> {
718-
static const int _MODE_INVALID_ = -2;
719-
static const int _MODE_DELETED_ = -1;
720-
static const int _MODE_MARKER_ = 0;
721-
static const int _MODE_PURE_FUNCTION_ = 1;
722-
static const int _MODE_FUNCTION_ = 2;
723-
static const int _MODE_PURE_FUNCTION_APPLY_ = 3;
724-
static const int _MODE_NULL_ = 4;
725-
static const int _MODE_FIELD_CLOSURE_ = 5;
726-
static const int _MODE_MAP_CLOSURE_ = 6;
727-
static const int _MODE_METHOD_ = 7;
728-
static const int _MODE_METHOD_INVOKE_ = 8;
717+
static const int _MODE_INVALID_ = -2;
718+
static const int _MODE_DELETED_ = -1;
719+
static const int _MODE_MARKER_ = 0;
720+
static const int _MODE_PURE_FUNCTION_ = 1;
721+
static const int _MODE_FUNCTION_ = 2;
722+
static const int _MODE_PURE_FUNCTION_APPLY_ = 3;
723+
static const int _MODE_NULL_ = 4;
724+
static const int _MODE_FIELD_OR_METHOD_CLOSURE_ = 5;
725+
static const int _MODE_METHOD_ = 6;
726+
static const int _MODE_FIELD_CLOSURE_ = 7;
727+
static const int _MODE_MAP_CLOSURE_ = 8;
729728
WatchGroup watchGrp;
730729
final _Handler handler;
731730
final List args;
732731
final Map<Symbol, dynamic> namedArgs = new Map<Symbol, dynamic>();
733732
final String name;
734733
int mode;
735-
/* dartbug.com/16401 Function*/ var fn;
734+
Function fn;
736735
FieldGetterFactory _fieldGetterFactory;
737736
bool dirtyArgs = true;
738737

@@ -789,13 +788,8 @@ class _EvalWatchRecord implements WatchRecord<_Handler> {
789788
if (value is Map) {
790789
mode = _MODE_MAP_CLOSURE_;
791790
} else {
792-
if (_fieldGetterFactory.isMethod(value, name)) {
793-
mode = _fieldGetterFactory.isMethodInvoke ? _MODE_METHOD_INVOKE_ : _MODE_METHOD_;
794-
fn = _fieldGetterFactory.method(value, name);
795-
} else {
796-
mode = _MODE_FIELD_CLOSURE_;
797-
fn = _fieldGetterFactory.getter(value, name);
798-
}
791+
mode = _MODE_FIELD_OR_METHOD_CLOSURE_;
792+
fn = _fieldGetterFactory.getter(value, name);
799793
}
800794
}
801795
}
@@ -820,19 +814,31 @@ class _EvalWatchRecord implements WatchRecord<_Handler> {
820814
value = (fn as FunctionApply).apply(args);
821815
dirtyArgs = false;
822816
break;
823-
case _MODE_FIELD_CLOSURE_:
817+
case _MODE_FIELD_OR_METHOD_CLOSURE_:
824818
var closure = fn(_object);
825-
value = closure == null ? null : Function.apply(closure, args, namedArgs);
826-
break;
827-
case _MODE_MAP_CLOSURE_:
828-
var closure = object[name];
829-
value = closure == null ? null : Function.apply(closure, args, namedArgs);
819+
// NOTE: When Dart looks up a method "foo" on object "x", it returns a
820+
// new closure for each lookup. They compare equal via "==" but are no
821+
// identical(). There's no point getting a new value each time and
822+
// decide it's the same so we'll skip further checking after the first
823+
// time.
824+
if (closure is Function && !identical(closure, fn(_object))) {
825+
fn = closure;
826+
mode = _MODE_METHOD_;
827+
} else {
828+
mode = _MODE_FIELD_CLOSURE_;
829+
}
830+
value = (closure == null) ? null : Function.apply(closure, args, namedArgs);
830831
break;
831832
case _MODE_METHOD_:
832833
value = Function.apply(fn, args, namedArgs);
833834
break;
834-
case _MODE_METHOD_INVOKE_:
835-
value = fn(args, namedArgs);
835+
case _MODE_FIELD_CLOSURE_:
836+
var closure = fn(_object);
837+
value = (closure == null) ? null : Function.apply(closure, args, namedArgs);
838+
break;
839+
case _MODE_MAP_CLOSURE_:
840+
var closure = object[name];
841+
value = (closure == null) ? null : Function.apply(closure, args, namedArgs);
836842
break;
837843
default:
838844
assert(false);

test/change_detection/dirty_checking_change_detector_spec.dart

+64-18
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,18 @@ import '../_specs.dart';
44
import 'package:angular/change_detection/change_detection.dart';
55
import 'package:angular/change_detection/dirty_checking_change_detector.dart';
66
import 'package:angular/change_detection/dirty_checking_change_detector_static.dart';
7+
import 'package:angular/change_detection/dirty_checking_change_detector_dynamic.dart';
78
import 'dart:collection';
89
import 'dart:math';
910

10-
void main() {
11-
describe('DirtyCheckingChangeDetector', () {
11+
void testWithGetterFactory(FieldGetterFactory getterFactory) {
12+
describe('DirtyCheckingChangeDetector with ${getterFactory.runtimeType}', () {
1213
DirtyCheckingChangeDetector<String> detector;
13-
FieldGetterFactory getterFactory = new StaticFieldGetterFactory({
14-
"first": (o) => o.first,
15-
"age": (o) => o.age,
16-
"last": (o) => o.last,
17-
"toString": (o) => o.toString
18-
});
1914

2015
beforeEach(() {
2116
detector = new DirtyCheckingChangeDetector<String>(getterFactory);
2217
});
2318

24-
describe('StaticFieldGetterFactory', () {
25-
it('should detect methods', () {
26-
var obj = new _User();
27-
expect(getterFactory.isMethod(obj, 'toString')).toEqual(true);
28-
expect(getterFactory.isMethod(obj, 'age')).toEqual(false);
29-
});
30-
});
31-
3219
describe('object field', () {
3320
it('should detect nothing', () {
3421
var changes = detector.collectChanges();
@@ -657,6 +644,42 @@ void main() {
657644
});
658645
});
659646

647+
describe('function watching', () {
648+
it('should detect no changes when watching a function', () {
649+
var user = new _User('marko', 'vuksanovic', 15);
650+
Iterator changeIterator;
651+
652+
detector..watch(user, 'isUnderAge', null);
653+
changeIterator = detector.collectChanges();
654+
expect(changeIterator.moveNext()).toEqual(true);
655+
expect(changeIterator.moveNext()).toEqual(false);
656+
657+
user.age = 17;
658+
changeIterator = detector.collectChanges();
659+
expect(changeIterator.moveNext()).toEqual(false);
660+
661+
user.age = 30;
662+
changeIterator = detector.collectChanges();
663+
expect(changeIterator.moveNext()).toEqual(false);
664+
});
665+
666+
it('should detect change when watching a property function', () {
667+
var user = new _User('marko', 'vuksanovic', 30);
668+
Iterator changeIterator;
669+
670+
detector..watch(user, 'isUnderAgeAsVariable', null);
671+
changeIterator = detector.collectChanges();
672+
expect(changeIterator.moveNext()).toEqual(true);
673+
674+
changeIterator = detector.collectChanges();
675+
expect(changeIterator.moveNext()).toEqual(false);
676+
677+
user.isUnderAgeAsVariable = () => false;
678+
changeIterator = detector.collectChanges();
679+
expect(changeIterator.moveNext()).toEqual(true);
680+
});
681+
});
682+
660683
describe('DuplicateMap', () {
661684
DuplicateMap map;
662685
beforeEach(() => map = new DuplicateMap());
@@ -689,12 +712,36 @@ void main() {
689712
});
690713
}
691714

715+
716+
void main() {
717+
testWithGetterFactory(new DynamicFieldGetterFactory());
718+
719+
testWithGetterFactory(new StaticFieldGetterFactory({
720+
"first": (o) => o.first,
721+
"age": (o) => o.age,
722+
"last": (o) => o.last,
723+
"toString": (o) => o.toString,
724+
"isUnderAge": (o) => o.isUnderAge,
725+
"isUnderAgeAsVariable": (o) => o.isUnderAgeAsVariable,
726+
}));
727+
}
728+
729+
692730
class _User {
693731
String first;
694732
String last;
695733
num age;
734+
var isUnderAgeAsVariable;
735+
List<String> list = ['foo', 'bar', 'baz'];
736+
Map map = {'foo': 'bar', 'baz': 'cux'};
737+
738+
_User([this.first, this.last, this.age]) {
739+
isUnderAgeAsVariable = isUnderAge;
740+
}
696741

697-
_User([this.first, this.last, this.age]);
742+
bool isUnderAge() {
743+
return age != null ? age < 18 : false;
744+
}
698745
}
699746

700747
Matcher toEqualCollectionRecord({collection, previous, additions, moves, removals}) =>
@@ -915,7 +962,6 @@ class MapRecordMatcher extends _CollectionMatcher<KeyValueRecord> {
915962
}
916963
}
917964

918-
919965
class FooBar {
920966
static int fooIds = 0;
921967

0 commit comments

Comments
 (0)