Index: pkg/observe/test/path_observer_test.dart |
diff --git a/pkg/observe/test/path_observer_test.dart b/pkg/observe/test/path_observer_test.dart |
index 674f8b27f0c470b51be97bfcbb1087758f131201..aae8887b657b2626edab0ffae5d4f0f6c03fcae0 100644 |
--- a/pkg/observe/test/path_observer_test.dart |
+++ b/pkg/observe/test/path_observer_test.dart |
@@ -5,6 +5,10 @@ |
import 'dart:async'; |
import 'package:observe/observe.dart'; |
import 'package:unittest/unittest.dart'; |
+import 'package:observe/src/path_observer.dart' |
+ show getSegmentsOfPropertyPathForTesting, |
+ observerSentinelForTesting; |
+ |
import 'observe_test_utils.dart'; |
// This file contains code ported from: |
@@ -17,34 +21,100 @@ main() => dirtyCheckZone().run(() { |
group('PropertyPath', () { |
test('toString length', () { |
- expectPath(p, str, len) { |
+ expectPath(p, str, len, [keys]) { |
var path = new PropertyPath(p); |
expect(path.toString(), str); |
expect(path.length, len, reason: 'expected path length $len for $path'); |
+ if (keys == null) { |
+ expect(path.isValid, isFalse); |
+ } else { |
+ expect(path.isValid, isTrue); |
+ expect(getSegmentsOfPropertyPathForTesting(path), keys); |
+ } |
} |
expectPath('/foo', '<invalid path>', 0); |
- expectPath('abc', 'abc', 1); |
- expectPath('a.b.c', 'a.b.c', 3); |
- expectPath('a.b.c ', 'a.b.c', 3); |
- expectPath(' a.b.c', 'a.b.c', 3); |
- expectPath(' a.b.c ', 'a.b.c', 3); |
- expectPath('1.abc', '1.abc', 2); |
- expectPath([#qux], 'qux', 1); |
- expectPath([1, #foo, #bar], '1.foo.bar', 3); |
+ expectPath('1.abc', '<invalid path>', 0); |
+ expectPath('abc', 'abc', 1, [#abc]); |
+ expectPath('a.b.c', 'a.b.c', 3, [#a, #b, #c]); |
+ expectPath('a.b.c ', 'a.b.c', 3, [#a, #b, #c]); |
+ expectPath(' a.b.c', 'a.b.c', 3, [#a, #b, #c]); |
+ expectPath(' a.b.c ', 'a.b.c', 3, [#a, #b, #c]); |
+ expectPath('[1].abc', '[1].abc', 2, [1, #abc]); |
+ expectPath([#qux], 'qux', 1, [#qux]); |
+ expectPath([1, #foo, #bar], '[1].foo.bar', 3, [1, #foo, #bar]); |
+ expectPath([1, #foo, 'bar'], '[1].foo["bar"]', 3, [1, #foo, 'bar']); |
+ |
+ // From test.js: "path validity" test: |
+ |
+ expectPath('', '', 0, []); |
+ expectPath(' ', '', 0, []); |
+ expectPath(null, '', 0, []); |
+ expectPath('a', 'a', 1, [#a]); |
+ expectPath('a.b', 'a.b', 2, [#a, #b]); |
+ expectPath('a. b', 'a.b', 2, [#a, #b]); |
+ expectPath('a .b', 'a.b', 2, [#a, #b]); |
+ expectPath('a . b', 'a.b', 2, [#a, #b]); |
+ expectPath(' a . b ', 'a.b', 2, [#a, #b]); |
+ expectPath('a[0]', 'a[0]', 2, [#a, 0]); |
+ expectPath('a [0]', 'a[0]', 2, [#a, 0]); |
+ expectPath('a[0][1]', 'a[0][1]', 3, [#a, 0, 1]); |
+ expectPath('a [ 0 ] [ 1 ] ', 'a[0][1]', 3, [#a, 0, 1]); |
+ expectPath('[1234567890] ', '[1234567890]', 1, [1234567890]); |
+ expectPath(' [1234567890] ', '[1234567890]', 1, [1234567890]); |
+ expectPath('opt0', 'opt0', 1, [#opt0]); |
+ // Dart note: Modified to avoid a private Dart symbol: |
+ expectPath(r'$foo.$bar.baz_', r'$foo.$bar.baz_', 3, |
+ [#$foo, #$bar, #baz_]); |
+ // Dart note: this test is different because we treat ["baz"] always as a |
+ // indexing operation. |
+ expectPath('foo["baz"]', 'foo.baz', 2, [#foo, #baz]); |
+ expectPath('foo["b\\"az"]', 'foo["b\\"az"]', 2, [#foo, 'b"az']); |
+ expectPath("foo['b\\'az']", 'foo["b\'az"]', 2, [#foo, "b'az"]); |
+ expectPath([#a, #b], 'a.b', 2, [#a, #b]); |
+ |
+ expectPath('.', '<invalid path>', 0); |
+ expectPath(' . ', '<invalid path>', 0); |
+ expectPath('..', '<invalid path>', 0); |
+ expectPath('a[4', '<invalid path>', 0); |
+ expectPath('a.b.', '<invalid path>', 0); |
+ expectPath('a,b', '<invalid path>', 0); |
+ expectPath('a["foo]', '<invalid path>', 0); |
+ expectPath('[0x04]', '<invalid path>', 0); |
+ expectPath('[0foo]', '<invalid path>', 0); |
+ expectPath('[foo-bar]', '<invalid path>', 0); |
+ expectPath('foo-bar', '<invalid path>', 0); |
+ expectPath('42', '<invalid path>', 0); |
+ expectPath('a[04]', '<invalid path>', 0); |
+ expectPath(' a [ 04 ]', '<invalid path>', 0); |
+ expectPath(' 42 ', '<invalid path>', 0); |
+ expectPath('foo["bar]', '<invalid path>', 0); |
+ expectPath("foo['bar]", '<invalid path>', 0); |
+ }); |
+ |
+ test('objects with toString are not supported', () { |
+ // Dart note: this was intentionally not ported. See path_observer.dart. |
+ expect(() => new PropertyPath([new Foo('a'), new Foo('b')]), throws); |
}); |
+ test('invalid path returns null value', () { |
+ var path = new PropertyPath('a b'); |
+ expect(path.isValid, isFalse); |
+ expect(path.getValueFrom({'a': {'b': 2}}), isNull); |
+ }); |
+ |
+ |
test('caching and ==', () { |
- var start = new PropertyPath('abc.0'); |
+ var start = new PropertyPath('abc[0]'); |
for (int i = 1; i <= 100; i++) { |
- expect(identical(new PropertyPath('abc.0'), start), true, |
+ expect(identical(new PropertyPath('abc[0]'), start), true, |
reason: 'should return identical path'); |
- var p = new PropertyPath('abc.$i'); |
+ var p = new PropertyPath('abc[$i]'); |
expect(identical(p, start), false, |
reason: 'different paths should not be merged'); |
} |
- var end = new PropertyPath('abc.0'); |
+ var end = new PropertyPath('abc[0]'); |
expect(identical(end, start), false, |
reason: 'first entry expired'); |
expect(end, start, reason: 'different instances are equal'); |
@@ -52,7 +122,7 @@ main() => dirtyCheckZone().run(() { |
test('hashCode equal', () { |
var a = new PropertyPath([#foo, 2, #bar]); |
- var b = new PropertyPath('foo.2.bar'); |
+ var b = new PropertyPath('foo[2].bar'); |
expect(identical(a, b), false, reason: 'only strings cached'); |
expect(a, b, reason: 'same paths are equal'); |
expect(a.hashCode, b.hashCode, reason: 'equal hashCodes'); |
@@ -68,6 +138,8 @@ main() => dirtyCheckZone().run(() { |
expect(a.hashCode, isNot(b.hashCode), reason: 'different hashCodes'); |
}); |
}); |
+ |
+ group('CompoundObserver', compoundObserverTests); |
}); |
observePathTests() { |
@@ -203,7 +275,7 @@ observePathTests() { |
test('Path Value With Indices', () { |
var model = toObservable([]); |
- var path = new PathObserver(model, '0'); |
+ var path = new PathObserver(model, '[0]'); |
path.open(expectAsync((x) { |
expect(path.value, 123); |
expect(x, 123); |
@@ -361,6 +433,215 @@ observePathTests() { |
expect(model.log, ['[]= bar 42']); |
model.log.clear(); |
}); |
+ |
+ test('regression for TemplateBinding#161', () { |
+ var model = toObservable({'obj': toObservable({'bar': false})}); |
+ var ob1 = new PathObserver(model, 'obj.bar'); |
+ var called = false; |
+ ob1.open(() { called = true; }); |
+ |
+ var obj2 = new PathObserver(model, 'obj'); |
+ obj2.open(() { model['obj']['bar'] = true; }); |
+ |
+ model['obj'] = toObservable({ 'obj': 'obj' }); |
+ |
+ return new Future(() {}) |
+ .then((_) => expect(called, true)); |
+ }); |
+} |
+ |
+compoundObserverTests() { |
+ var model; |
+ var observer; |
+ bool called; |
+ var newValues; |
+ var oldValues; |
+ var observed; |
+ |
+ setUp(() { |
+ model = new TestModel(1, 2, 3); |
+ called = false; |
+ }); |
+ |
+ callback(a, b, c) { |
+ called = true; |
+ newValues = a; |
+ oldValues = b; |
+ observed = c; |
+ } |
+ |
+ reset() { |
+ called = false; |
+ newValues = null; |
+ oldValues = null; |
+ observed = null; |
+ } |
+ |
+ expectNoChanges() { |
+ observer.deliver(); |
+ expect(called, isFalse); |
+ expect(newValues, isNull); |
+ expect(oldValues, isNull); |
+ expect(observed, isNull); |
+ } |
+ |
+ expectCompoundPathChanges(expectedNewValues, |
+ expectedOldValues, expectedObserved, {deliver: true}) { |
+ if (deliver) observer.deliver(); |
+ expect(called, isTrue); |
+ |
+ expect(newValues, expectedNewValues); |
+ var oldValuesAsMap = {}; |
+ for (int i = 0; i < expectedOldValues.length; i++) { |
+ if (expectedOldValues[i] != null) { |
+ oldValuesAsMap[i] = expectedOldValues[i]; |
+ } |
+ } |
+ expect(oldValues, oldValuesAsMap); |
+ expect(observed, expectedObserved); |
+ |
+ reset(); |
+ } |
+ |
+ tearDown(() { |
+ observer.close(); |
+ reset(); |
+ }); |
+ |
+ _path(s) => new PropertyPath(s); |
+ |
+ test('simple', () { |
+ observer = new CompoundObserver(); |
+ observer.addPath(model, 'a'); |
+ observer.addPath(model, 'b'); |
+ observer.addPath(model, _path('c')); |
+ observer.open(callback); |
+ expectNoChanges(); |
+ |
+ var expectedObs = [model, _path('a'), model, _path('b'), model, _path('c')]; |
+ model.a = -10; |
+ model.b = 20; |
+ model.c = 30; |
+ expectCompoundPathChanges([-10, 20, 30], [1, 2, 3], expectedObs); |
+ |
+ model.a = 'a'; |
+ model.c = 'c'; |
+ expectCompoundPathChanges(['a', 20, 'c'], [-10, null, 30], expectedObs); |
+ |
+ model.a = 2; |
+ model.b = 3; |
+ model.c = 4; |
+ expectCompoundPathChanges([2, 3, 4], ['a', 20, 'c'], expectedObs); |
+ |
+ model.a = 'z'; |
+ model.b = 'y'; |
+ model.c = 'x'; |
+ expect(observer.value, ['z', 'y', 'x']); |
+ expectNoChanges(); |
+ |
+ expect(model.a, 'z'); |
+ expect(model.b, 'y'); |
+ expect(model.c, 'x'); |
+ expectNoChanges(); |
+ }); |
+ |
+ test('reportChangesOnOpen', () { |
+ observer = new CompoundObserver(true); |
+ observer.addPath(model, 'a'); |
+ observer.addPath(model, 'b'); |
+ observer.addPath(model, _path('c')); |
+ |
+ model.a = -10; |
+ model.b = 20; |
+ observer.open(callback); |
+ var expectedObs = [model, _path('a'), model, _path('b'), model, _path('c')]; |
+ expectCompoundPathChanges([-10, 20, 3], [1, 2, null], expectedObs, |
+ deliver: false); |
+ }); |
+ |
+ test('All Observers', () { |
+ observer = new CompoundObserver(); |
+ var pathObserver1 = new PathObserver(model, 'a'); |
+ var pathObserver2 = new PathObserver(model, 'b'); |
+ var pathObserver3 = new PathObserver(model, _path('c')); |
+ |
+ observer.addObserver(pathObserver1); |
+ observer.addObserver(pathObserver2); |
+ observer.addObserver(pathObserver3); |
+ observer.open(callback); |
+ |
+ var expectedObs = [observerSentinelForTesting, pathObserver1, |
+ observerSentinelForTesting, pathObserver2, |
+ observerSentinelForTesting, pathObserver3]; |
+ model.a = -10; |
+ model.b = 20; |
+ model.c = 30; |
+ expectCompoundPathChanges([-10, 20, 30], [1, 2, 3], expectedObs); |
+ |
+ model.a = 'a'; |
+ model.c = 'c'; |
+ expectCompoundPathChanges(['a', 20, 'c'], [-10, null, 30], expectedObs); |
+ }); |
+ |
+ test('Degenerate Values', () { |
+ observer = new CompoundObserver(); |
+ observer.addPath(model, '.'); // invalid path |
+ observer.addPath('obj-value', ''); // empty path |
+ // Dart note: we don't port these two tests because in Dart we produce |
+ // exceptions for these invalid paths. |
+ // observer.addPath(model, 'foo'); // unreachable |
+ // observer.addPath(3, 'bar'); // non-object with non-empty path |
+ var values = observer.open(callback); |
+ expect(values.length, 2); |
+ expect(values[0], null); |
+ expect(values[1], 'obj-value'); |
+ observer.close(); |
+ }); |
+ |
+ test('Heterogeneous', () { |
+ model.c = null; |
+ var otherModel = new TestModel(null, null, 3); |
+ |
+ twice(value) => value * 2; |
+ half(value) => value ~/ 2; |
+ |
+ var compound = new CompoundObserver(); |
+ compound.addPath(model, 'a'); |
+ compound.addObserver(new ObserverTransform(new PathObserver(model, 'b'), |
+ twice, setValue: half)); |
+ compound.addObserver(new PathObserver(otherModel, 'c')); |
+ |
+ combine(values) => values[0] + values[1] + values[2]; |
+ observer = new ObserverTransform(compound, combine); |
+ |
+ var newValue; |
+ transformCallback(v) { |
+ newValue = v; |
+ called = true; |
+ } |
+ expect(observer.open(transformCallback), 8); |
+ |
+ model.a = 2; |
+ model.b = 4; |
+ observer.deliver(); |
+ expect(called, isTrue); |
+ expect(newValue, 13); |
+ called = false; |
+ |
+ model.b = 10; |
+ otherModel.c = 5; |
+ observer.deliver(); |
+ expect(called, isTrue); |
+ expect(newValue, 27); |
+ called = false; |
+ |
+ model.a = 20; |
+ model.b = 1; |
+ otherModel.c = 5; |
+ observer.deliver(); |
+ expect(called, isFalse); |
+ expect(newValue, 27); |
+ }); |
} |
/// A matcher that checks that a closure throws a NoSuchMethodError matching the |
@@ -426,7 +707,7 @@ class IndexerModel implements Indexable<String, dynamic> { |
class TestModel extends ChangeNotifier { |
var _a, _b, _c; |
- TestModel(); |
+ TestModel([this._a, this._b, this._c]); |
get a => _a; |
@@ -456,3 +737,9 @@ class WatcherModel extends Observable { |
WatcherModel(); |
} |
+ |
+class Foo { |
+ var value; |
+ Foo(this.value); |
+ String toString() => 'Foo$value'; |
+} |