| 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';
|
| +}
|
|
|