Chromium Code Reviews| 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..bdbbeadd16980c3ac21ccdfa20960eee3c6bead8 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,214 @@ 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, [dontDeliver = false]) { |
|
jakemac
2014/07/25 16:40:33
[deliver = true] would be easier to comprehend her
Siggi Cherem (dart-lang)
2014/07/25 17:45:12
agree :) - I renamed it and made it a named argume
|
| + if (!dontDeliver) 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, true); |
| + }); |
| + |
| + 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 +706,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 +736,9 @@ class WatcherModel extends Observable { |
| WatcherModel(); |
| } |
| + |
| +class Foo { |
| + var value; |
| + Foo(this.value); |
| + String toString() => 'Foo$value'; |
| +} |