OLD | NEW |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 import 'dart:async'; | 5 import 'dart:async'; |
6 import 'package:observe/observe.dart'; | 6 import 'package:observe/observe.dart'; |
7 import 'package:unittest/unittest.dart'; | 7 import 'package:unittest/unittest.dart'; |
| 8 import 'package:observe/src/path_observer.dart' |
| 9 show getSegmentsOfPropertyPathForTesting, |
| 10 observerSentinelForTesting; |
| 11 |
8 import 'observe_test_utils.dart'; | 12 import 'observe_test_utils.dart'; |
9 | 13 |
10 // This file contains code ported from: | 14 // This file contains code ported from: |
11 // https://github.com/rafaelw/ChangeSummary/blob/master/tests/test.js | 15 // https://github.com/rafaelw/ChangeSummary/blob/master/tests/test.js |
12 // Dart note: getting invalid properties is an error, unlike in JS where it | 16 // Dart note: getting invalid properties is an error, unlike in JS where it |
13 // returns undefined. This difference comes up where we check for _throwsNSM in | 17 // returns undefined. This difference comes up where we check for _throwsNSM in |
14 // the tests below. | 18 // the tests below. |
15 main() => dirtyCheckZone().run(() { | 19 main() => dirtyCheckZone().run(() { |
16 group('PathObserver', observePathTests); | 20 group('PathObserver', observePathTests); |
17 | 21 |
18 group('PropertyPath', () { | 22 group('PropertyPath', () { |
19 test('toString length', () { | 23 test('toString length', () { |
20 expectPath(p, str, len) { | 24 expectPath(p, str, len, [keys]) { |
21 var path = new PropertyPath(p); | 25 var path = new PropertyPath(p); |
22 expect(path.toString(), str); | 26 expect(path.toString(), str); |
23 expect(path.length, len, reason: 'expected path length $len for $path'); | 27 expect(path.length, len, reason: 'expected path length $len for $path'); |
| 28 if (keys == null) { |
| 29 expect(path.isValid, isFalse); |
| 30 } else { |
| 31 expect(path.isValid, isTrue); |
| 32 expect(getSegmentsOfPropertyPathForTesting(path), keys); |
| 33 } |
24 } | 34 } |
25 | 35 |
26 expectPath('/foo', '<invalid path>', 0); | 36 expectPath('/foo', '<invalid path>', 0); |
27 expectPath('abc', 'abc', 1); | 37 expectPath('1.abc', '<invalid path>', 0); |
28 expectPath('a.b.c', 'a.b.c', 3); | 38 expectPath('abc', 'abc', 1, [#abc]); |
29 expectPath('a.b.c ', 'a.b.c', 3); | 39 expectPath('a.b.c', 'a.b.c', 3, [#a, #b, #c]); |
30 expectPath(' a.b.c', 'a.b.c', 3); | 40 expectPath('a.b.c ', 'a.b.c', 3, [#a, #b, #c]); |
31 expectPath(' a.b.c ', 'a.b.c', 3); | 41 expectPath(' a.b.c', 'a.b.c', 3, [#a, #b, #c]); |
32 expectPath('1.abc', '1.abc', 2); | 42 expectPath(' a.b.c ', 'a.b.c', 3, [#a, #b, #c]); |
33 expectPath([#qux], 'qux', 1); | 43 expectPath('[1].abc', '[1].abc', 2, [1, #abc]); |
34 expectPath([1, #foo, #bar], '1.foo.bar', 3); | 44 expectPath([#qux], 'qux', 1, [#qux]); |
| 45 expectPath([1, #foo, #bar], '[1].foo.bar', 3, [1, #foo, #bar]); |
| 46 expectPath([1, #foo, 'bar'], '[1].foo["bar"]', 3, [1, #foo, 'bar']); |
| 47 |
| 48 // From test.js: "path validity" test: |
| 49 |
| 50 expectPath('', '', 0, []); |
| 51 expectPath(' ', '', 0, []); |
| 52 expectPath(null, '', 0, []); |
| 53 expectPath('a', 'a', 1, [#a]); |
| 54 expectPath('a.b', 'a.b', 2, [#a, #b]); |
| 55 expectPath('a. b', 'a.b', 2, [#a, #b]); |
| 56 expectPath('a .b', 'a.b', 2, [#a, #b]); |
| 57 expectPath('a . b', 'a.b', 2, [#a, #b]); |
| 58 expectPath(' a . b ', 'a.b', 2, [#a, #b]); |
| 59 expectPath('a[0]', 'a[0]', 2, [#a, 0]); |
| 60 expectPath('a [0]', 'a[0]', 2, [#a, 0]); |
| 61 expectPath('a[0][1]', 'a[0][1]', 3, [#a, 0, 1]); |
| 62 expectPath('a [ 0 ] [ 1 ] ', 'a[0][1]', 3, [#a, 0, 1]); |
| 63 expectPath('[1234567890] ', '[1234567890]', 1, [1234567890]); |
| 64 expectPath(' [1234567890] ', '[1234567890]', 1, [1234567890]); |
| 65 expectPath('opt0', 'opt0', 1, [#opt0]); |
| 66 // Dart note: Modified to avoid a private Dart symbol: |
| 67 expectPath(r'$foo.$bar.baz_', r'$foo.$bar.baz_', 3, |
| 68 [#$foo, #$bar, #baz_]); |
| 69 // Dart note: this test is different because we treat ["baz"] always as a |
| 70 // indexing operation. |
| 71 expectPath('foo["baz"]', 'foo.baz', 2, [#foo, #baz]); |
| 72 expectPath('foo["b\\"az"]', 'foo["b\\"az"]', 2, [#foo, 'b"az']); |
| 73 expectPath("foo['b\\'az']", 'foo["b\'az"]', 2, [#foo, "b'az"]); |
| 74 expectPath([#a, #b], 'a.b', 2, [#a, #b]); |
| 75 |
| 76 expectPath('.', '<invalid path>', 0); |
| 77 expectPath(' . ', '<invalid path>', 0); |
| 78 expectPath('..', '<invalid path>', 0); |
| 79 expectPath('a[4', '<invalid path>', 0); |
| 80 expectPath('a.b.', '<invalid path>', 0); |
| 81 expectPath('a,b', '<invalid path>', 0); |
| 82 expectPath('a["foo]', '<invalid path>', 0); |
| 83 expectPath('[0x04]', '<invalid path>', 0); |
| 84 expectPath('[0foo]', '<invalid path>', 0); |
| 85 expectPath('[foo-bar]', '<invalid path>', 0); |
| 86 expectPath('foo-bar', '<invalid path>', 0); |
| 87 expectPath('42', '<invalid path>', 0); |
| 88 expectPath('a[04]', '<invalid path>', 0); |
| 89 expectPath(' a [ 04 ]', '<invalid path>', 0); |
| 90 expectPath(' 42 ', '<invalid path>', 0); |
| 91 expectPath('foo["bar]', '<invalid path>', 0); |
| 92 expectPath("foo['bar]", '<invalid path>', 0); |
35 }); | 93 }); |
36 | 94 |
| 95 test('objects with toString are not supported', () { |
| 96 // Dart note: this was intentionally not ported. See path_observer.dart. |
| 97 expect(() => new PropertyPath([new Foo('a'), new Foo('b')]), throws); |
| 98 }); |
| 99 |
| 100 test('invalid path returns null value', () { |
| 101 var path = new PropertyPath('a b'); |
| 102 expect(path.isValid, isFalse); |
| 103 expect(path.getValueFrom({'a': {'b': 2}}), isNull); |
| 104 }); |
| 105 |
| 106 |
37 test('caching and ==', () { | 107 test('caching and ==', () { |
38 var start = new PropertyPath('abc.0'); | 108 var start = new PropertyPath('abc[0]'); |
39 for (int i = 1; i <= 100; i++) { | 109 for (int i = 1; i <= 100; i++) { |
40 expect(identical(new PropertyPath('abc.0'), start), true, | 110 expect(identical(new PropertyPath('abc[0]'), start), true, |
41 reason: 'should return identical path'); | 111 reason: 'should return identical path'); |
42 | 112 |
43 var p = new PropertyPath('abc.$i'); | 113 var p = new PropertyPath('abc[$i]'); |
44 expect(identical(p, start), false, | 114 expect(identical(p, start), false, |
45 reason: 'different paths should not be merged'); | 115 reason: 'different paths should not be merged'); |
46 } | 116 } |
47 var end = new PropertyPath('abc.0'); | 117 var end = new PropertyPath('abc[0]'); |
48 expect(identical(end, start), false, | 118 expect(identical(end, start), false, |
49 reason: 'first entry expired'); | 119 reason: 'first entry expired'); |
50 expect(end, start, reason: 'different instances are equal'); | 120 expect(end, start, reason: 'different instances are equal'); |
51 }); | 121 }); |
52 | 122 |
53 test('hashCode equal', () { | 123 test('hashCode equal', () { |
54 var a = new PropertyPath([#foo, 2, #bar]); | 124 var a = new PropertyPath([#foo, 2, #bar]); |
55 var b = new PropertyPath('foo.2.bar'); | 125 var b = new PropertyPath('foo[2].bar'); |
56 expect(identical(a, b), false, reason: 'only strings cached'); | 126 expect(identical(a, b), false, reason: 'only strings cached'); |
57 expect(a, b, reason: 'same paths are equal'); | 127 expect(a, b, reason: 'same paths are equal'); |
58 expect(a.hashCode, b.hashCode, reason: 'equal hashCodes'); | 128 expect(a.hashCode, b.hashCode, reason: 'equal hashCodes'); |
59 }); | 129 }); |
60 | 130 |
61 test('hashCode not equal', () { | 131 test('hashCode not equal', () { |
62 expect(2.hashCode, isNot(3.hashCode), | 132 expect(2.hashCode, isNot(3.hashCode), |
63 reason: 'test depends on 2 and 3 having different hashcodes'); | 133 reason: 'test depends on 2 and 3 having different hashcodes'); |
64 | 134 |
65 var a = new PropertyPath([2]); | 135 var a = new PropertyPath([2]); |
66 var b = new PropertyPath([3]); | 136 var b = new PropertyPath([3]); |
67 expect(a, isNot(b), reason: 'different paths'); | 137 expect(a, isNot(b), reason: 'different paths'); |
68 expect(a.hashCode, isNot(b.hashCode), reason: 'different hashCodes'); | 138 expect(a.hashCode, isNot(b.hashCode), reason: 'different hashCodes'); |
69 }); | 139 }); |
70 }); | 140 }); |
| 141 |
| 142 group('CompoundObserver', compoundObserverTests); |
71 }); | 143 }); |
72 | 144 |
73 observePathTests() { | 145 observePathTests() { |
74 test('Degenerate Values', () { | 146 test('Degenerate Values', () { |
75 expect(new PathObserver(null, '').value, null); | 147 expect(new PathObserver(null, '').value, null); |
76 expect(new PathObserver(123, '').value, 123); | 148 expect(new PathObserver(123, '').value, 123); |
77 expect(() => new PathObserver(123, 'foo.bar.baz').value, _throwsNSM('foo')); | 149 expect(() => new PathObserver(123, 'foo.bar.baz').value, _throwsNSM('foo')); |
78 | 150 |
79 // shouldn't throw: | 151 // shouldn't throw: |
80 new PathObserver(123, '')..open((_) {})..close(); | 152 new PathObserver(123, '')..open((_) {})..close(); |
(...skipping 115 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
196 arr['bat'] = 'boot'; | 268 arr['bat'] = 'boot'; |
197 | 269 |
198 }).then(newMicrotask).then((_) { | 270 }).then(newMicrotask).then((_) { |
199 expect(fooValues, ['baz']); | 271 expect(fooValues, ['baz']); |
200 expect(batValues, []); | 272 expect(batValues, []); |
201 }); | 273 }); |
202 }); | 274 }); |
203 | 275 |
204 test('Path Value With Indices', () { | 276 test('Path Value With Indices', () { |
205 var model = toObservable([]); | 277 var model = toObservable([]); |
206 var path = new PathObserver(model, '0'); | 278 var path = new PathObserver(model, '[0]'); |
207 path.open(expectAsync((x) { | 279 path.open(expectAsync((x) { |
208 expect(path.value, 123); | 280 expect(path.value, 123); |
209 expect(x, 123); | 281 expect(x, 123); |
210 })); | 282 })); |
211 model.add(123); | 283 model.add(123); |
212 }); | 284 }); |
213 | 285 |
214 group('ObservableList', () { | 286 group('ObservableList', () { |
215 test('isNotEmpty', () { | 287 test('isNotEmpty', () { |
216 var model = new ObservableList(); | 288 var model = new ObservableList(); |
(...skipping 137 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
354 model.log.clear(); | 426 model.log.clear(); |
355 observer = new PathObserver(model, 'bar'); | 427 observer = new PathObserver(model, 'bar'); |
356 expect(observer.value, null, reason: 'path not found'); | 428 expect(observer.value, null, reason: 'path not found'); |
357 expect(model.log, ['[] bar']); | 429 expect(model.log, ['[] bar']); |
358 model.log.clear(); | 430 model.log.clear(); |
359 | 431 |
360 observer.value = 42; | 432 observer.value = 42; |
361 expect(model.log, ['[]= bar 42']); | 433 expect(model.log, ['[]= bar 42']); |
362 model.log.clear(); | 434 model.log.clear(); |
363 }); | 435 }); |
| 436 |
| 437 test('regression for TemplateBinding#161', () { |
| 438 var model = toObservable({'obj': toObservable({'bar': false})}); |
| 439 var ob1 = new PathObserver(model, 'obj.bar'); |
| 440 var called = false; |
| 441 ob1.open(() { called = true; }); |
| 442 |
| 443 var obj2 = new PathObserver(model, 'obj'); |
| 444 obj2.open(() { model['obj']['bar'] = true; }); |
| 445 |
| 446 model['obj'] = toObservable({ 'obj': 'obj' }); |
| 447 |
| 448 return new Future(() {}) |
| 449 .then((_) => expect(called, true)); |
| 450 }); |
364 } | 451 } |
365 | 452 |
| 453 compoundObserverTests() { |
| 454 var model; |
| 455 var observer; |
| 456 bool called; |
| 457 var newValues; |
| 458 var oldValues; |
| 459 var observed; |
| 460 |
| 461 setUp(() { |
| 462 model = new TestModel(1, 2, 3); |
| 463 called = false; |
| 464 }); |
| 465 |
| 466 callback(a, b, c) { |
| 467 called = true; |
| 468 newValues = a; |
| 469 oldValues = b; |
| 470 observed = c; |
| 471 } |
| 472 |
| 473 reset() { |
| 474 called = false; |
| 475 newValues = null; |
| 476 oldValues = null; |
| 477 observed = null; |
| 478 } |
| 479 |
| 480 expectNoChanges() { |
| 481 observer.deliver(); |
| 482 expect(called, isFalse); |
| 483 expect(newValues, isNull); |
| 484 expect(oldValues, isNull); |
| 485 expect(observed, isNull); |
| 486 } |
| 487 |
| 488 expectCompoundPathChanges(expectedNewValues, |
| 489 expectedOldValues, expectedObserved, {deliver: true}) { |
| 490 if (deliver) observer.deliver(); |
| 491 expect(called, isTrue); |
| 492 |
| 493 expect(newValues, expectedNewValues); |
| 494 var oldValuesAsMap = {}; |
| 495 for (int i = 0; i < expectedOldValues.length; i++) { |
| 496 if (expectedOldValues[i] != null) { |
| 497 oldValuesAsMap[i] = expectedOldValues[i]; |
| 498 } |
| 499 } |
| 500 expect(oldValues, oldValuesAsMap); |
| 501 expect(observed, expectedObserved); |
| 502 |
| 503 reset(); |
| 504 } |
| 505 |
| 506 tearDown(() { |
| 507 observer.close(); |
| 508 reset(); |
| 509 }); |
| 510 |
| 511 _path(s) => new PropertyPath(s); |
| 512 |
| 513 test('simple', () { |
| 514 observer = new CompoundObserver(); |
| 515 observer.addPath(model, 'a'); |
| 516 observer.addPath(model, 'b'); |
| 517 observer.addPath(model, _path('c')); |
| 518 observer.open(callback); |
| 519 expectNoChanges(); |
| 520 |
| 521 var expectedObs = [model, _path('a'), model, _path('b'), model, _path('c')]; |
| 522 model.a = -10; |
| 523 model.b = 20; |
| 524 model.c = 30; |
| 525 expectCompoundPathChanges([-10, 20, 30], [1, 2, 3], expectedObs); |
| 526 |
| 527 model.a = 'a'; |
| 528 model.c = 'c'; |
| 529 expectCompoundPathChanges(['a', 20, 'c'], [-10, null, 30], expectedObs); |
| 530 |
| 531 model.a = 2; |
| 532 model.b = 3; |
| 533 model.c = 4; |
| 534 expectCompoundPathChanges([2, 3, 4], ['a', 20, 'c'], expectedObs); |
| 535 |
| 536 model.a = 'z'; |
| 537 model.b = 'y'; |
| 538 model.c = 'x'; |
| 539 expect(observer.value, ['z', 'y', 'x']); |
| 540 expectNoChanges(); |
| 541 |
| 542 expect(model.a, 'z'); |
| 543 expect(model.b, 'y'); |
| 544 expect(model.c, 'x'); |
| 545 expectNoChanges(); |
| 546 }); |
| 547 |
| 548 test('reportChangesOnOpen', () { |
| 549 observer = new CompoundObserver(true); |
| 550 observer.addPath(model, 'a'); |
| 551 observer.addPath(model, 'b'); |
| 552 observer.addPath(model, _path('c')); |
| 553 |
| 554 model.a = -10; |
| 555 model.b = 20; |
| 556 observer.open(callback); |
| 557 var expectedObs = [model, _path('a'), model, _path('b'), model, _path('c')]; |
| 558 expectCompoundPathChanges([-10, 20, 3], [1, 2, null], expectedObs, |
| 559 deliver: false); |
| 560 }); |
| 561 |
| 562 test('All Observers', () { |
| 563 observer = new CompoundObserver(); |
| 564 var pathObserver1 = new PathObserver(model, 'a'); |
| 565 var pathObserver2 = new PathObserver(model, 'b'); |
| 566 var pathObserver3 = new PathObserver(model, _path('c')); |
| 567 |
| 568 observer.addObserver(pathObserver1); |
| 569 observer.addObserver(pathObserver2); |
| 570 observer.addObserver(pathObserver3); |
| 571 observer.open(callback); |
| 572 |
| 573 var expectedObs = [observerSentinelForTesting, pathObserver1, |
| 574 observerSentinelForTesting, pathObserver2, |
| 575 observerSentinelForTesting, pathObserver3]; |
| 576 model.a = -10; |
| 577 model.b = 20; |
| 578 model.c = 30; |
| 579 expectCompoundPathChanges([-10, 20, 30], [1, 2, 3], expectedObs); |
| 580 |
| 581 model.a = 'a'; |
| 582 model.c = 'c'; |
| 583 expectCompoundPathChanges(['a', 20, 'c'], [-10, null, 30], expectedObs); |
| 584 }); |
| 585 |
| 586 test('Degenerate Values', () { |
| 587 observer = new CompoundObserver(); |
| 588 observer.addPath(model, '.'); // invalid path |
| 589 observer.addPath('obj-value', ''); // empty path |
| 590 // Dart note: we don't port these two tests because in Dart we produce |
| 591 // exceptions for these invalid paths. |
| 592 // observer.addPath(model, 'foo'); // unreachable |
| 593 // observer.addPath(3, 'bar'); // non-object with non-empty path |
| 594 var values = observer.open(callback); |
| 595 expect(values.length, 2); |
| 596 expect(values[0], null); |
| 597 expect(values[1], 'obj-value'); |
| 598 observer.close(); |
| 599 }); |
| 600 |
| 601 test('Heterogeneous', () { |
| 602 model.c = null; |
| 603 var otherModel = new TestModel(null, null, 3); |
| 604 |
| 605 twice(value) => value * 2; |
| 606 half(value) => value ~/ 2; |
| 607 |
| 608 var compound = new CompoundObserver(); |
| 609 compound.addPath(model, 'a'); |
| 610 compound.addObserver(new ObserverTransform(new PathObserver(model, 'b'), |
| 611 twice, setValue: half)); |
| 612 compound.addObserver(new PathObserver(otherModel, 'c')); |
| 613 |
| 614 combine(values) => values[0] + values[1] + values[2]; |
| 615 observer = new ObserverTransform(compound, combine); |
| 616 |
| 617 var newValue; |
| 618 transformCallback(v) { |
| 619 newValue = v; |
| 620 called = true; |
| 621 } |
| 622 expect(observer.open(transformCallback), 8); |
| 623 |
| 624 model.a = 2; |
| 625 model.b = 4; |
| 626 observer.deliver(); |
| 627 expect(called, isTrue); |
| 628 expect(newValue, 13); |
| 629 called = false; |
| 630 |
| 631 model.b = 10; |
| 632 otherModel.c = 5; |
| 633 observer.deliver(); |
| 634 expect(called, isTrue); |
| 635 expect(newValue, 27); |
| 636 called = false; |
| 637 |
| 638 model.a = 20; |
| 639 model.b = 1; |
| 640 otherModel.c = 5; |
| 641 observer.deliver(); |
| 642 expect(called, isFalse); |
| 643 expect(newValue, 27); |
| 644 }); |
| 645 } |
| 646 |
366 /// A matcher that checks that a closure throws a NoSuchMethodError matching the | 647 /// A matcher that checks that a closure throws a NoSuchMethodError matching the |
367 /// given [name]. | 648 /// given [name]. |
368 _throwsNSM(String name) => throwsA(_isNoSuchMethodOf(name)); | 649 _throwsNSM(String name) => throwsA(_isNoSuchMethodOf(name)); |
369 | 650 |
370 /// A matcher that checkes whether an exception is a NoSuchMethodError matching | 651 /// A matcher that checkes whether an exception is a NoSuchMethodError matching |
371 /// the given [name]. | 652 /// the given [name]. |
372 _isNoSuchMethodOf(String name) => predicate((e) => | 653 _isNoSuchMethodOf(String name) => predicate((e) => |
373 e is NoSuchMethodError && | 654 e is NoSuchMethodError && |
374 // Dart2js and VM error messages are a bit different, but they both contain | 655 // Dart2js and VM error messages are a bit different, but they both contain |
375 // the name. | 656 // the name. |
(...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
419 operator []=(index, value) { | 700 operator []=(index, value) { |
420 log.add('[]= $index $value'); | 701 log.add('[]= $index $value'); |
421 if (index == 'foo') _foo = value; | 702 if (index == 'foo') _foo = value; |
422 } | 703 } |
423 } | 704 } |
424 | 705 |
425 @reflectable | 706 @reflectable |
426 class TestModel extends ChangeNotifier { | 707 class TestModel extends ChangeNotifier { |
427 var _a, _b, _c; | 708 var _a, _b, _c; |
428 | 709 |
429 TestModel(); | 710 TestModel([this._a, this._b, this._c]); |
430 | 711 |
431 get a => _a; | 712 get a => _a; |
432 | 713 |
433 void set a(newValue) { | 714 void set a(newValue) { |
434 _a = notifyPropertyChange(#a, _a, newValue); | 715 _a = notifyPropertyChange(#a, _a, newValue); |
435 } | 716 } |
436 | 717 |
437 get b => _b; | 718 get b => _b; |
438 | 719 |
439 void set b(newValue) { | 720 void set b(newValue) { |
440 _b = notifyPropertyChange(#b, _b, newValue); | 721 _b = notifyPropertyChange(#b, _b, newValue); |
441 } | 722 } |
442 | 723 |
443 get c => _c; | 724 get c => _c; |
444 | 725 |
445 void set c(newValue) { | 726 void set c(newValue) { |
446 _c = notifyPropertyChange(#c, _c, newValue); | 727 _c = notifyPropertyChange(#c, _c, newValue); |
447 } | 728 } |
448 } | 729 } |
449 | 730 |
450 class WatcherModel extends Observable { | 731 class WatcherModel extends Observable { |
451 // TODO(jmesserly): dart2js does not let these be on the same line: | 732 // TODO(jmesserly): dart2js does not let these be on the same line: |
452 // @observable var a, b, c; | 733 // @observable var a, b, c; |
453 @observable var a; | 734 @observable var a; |
454 @observable var b; | 735 @observable var b; |
455 @observable var c; | 736 @observable var c; |
456 | 737 |
457 WatcherModel(); | 738 WatcherModel(); |
458 } | 739 } |
| 740 |
| 741 class Foo { |
| 742 var value; |
| 743 Foo(this.value); |
| 744 String toString() => 'Foo$value'; |
| 745 } |
OLD | NEW |