OLD | NEW |
(Empty) | |
| 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 |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 import 'dart:async'; |
| 6 import 'package:observe/observe.dart'; |
| 7 import 'package:unittest/unittest.dart'; |
| 8 import 'package:observe/src/path_observer.dart' |
| 9 show getSegmentsOfPropertyPathForTesting, |
| 10 observerSentinelForTesting; |
| 11 |
| 12 import 'observe_test_utils.dart'; |
| 13 |
| 14 import 'package:observe/mirrors_used.dart'; // make test smaller. |
| 15 import 'package:smoke/mirrors.dart'; |
| 16 |
| 17 // This file contains code ported from: |
| 18 // https://github.com/rafaelw/ChangeSummary/blob/master/tests/test.js |
| 19 // Dart note: getting invalid properties is an error, unlike in JS where it |
| 20 // returns undefined. This difference comes up where we check for _throwsNSM in |
| 21 // the tests below. |
| 22 main() => dirtyCheckZone().run(() { |
| 23 useMirrors(); |
| 24 |
| 25 group('PathObserver', observePathTests); |
| 26 |
| 27 group('PropertyPath', () { |
| 28 test('toString length', () { |
| 29 expectPath(p, str, len, [keys]) { |
| 30 var path = new PropertyPath(p); |
| 31 expect(path.toString(), str); |
| 32 expect(path.length, len, reason: 'expected path length $len for $path'); |
| 33 if (keys == null) { |
| 34 expect(path.isValid, isFalse); |
| 35 } else { |
| 36 expect(path.isValid, isTrue); |
| 37 expect(getSegmentsOfPropertyPathForTesting(path), keys); |
| 38 } |
| 39 } |
| 40 |
| 41 expectPath('/foo', '<invalid path>', 0); |
| 42 expectPath('1.abc', '<invalid path>', 0); |
| 43 expectPath('abc', 'abc', 1, [#abc]); |
| 44 expectPath('a.b.c', 'a.b.c', 3, [#a, #b, #c]); |
| 45 expectPath('a.b.c ', 'a.b.c', 3, [#a, #b, #c]); |
| 46 expectPath(' a.b.c', 'a.b.c', 3, [#a, #b, #c]); |
| 47 expectPath(' a.b.c ', 'a.b.c', 3, [#a, #b, #c]); |
| 48 expectPath('[1].abc', '[1].abc', 2, [1, #abc]); |
| 49 expectPath([#qux], 'qux', 1, [#qux]); |
| 50 expectPath([1, #foo, #bar], '[1].foo.bar', 3, [1, #foo, #bar]); |
| 51 expectPath([1, #foo, 'bar'], '[1].foo["bar"]', 3, [1, #foo, 'bar']); |
| 52 |
| 53 // From test.js: "path validity" test: |
| 54 |
| 55 expectPath('', '', 0, []); |
| 56 expectPath(' ', '', 0, []); |
| 57 expectPath(null, '', 0, []); |
| 58 expectPath('a', 'a', 1, [#a]); |
| 59 expectPath('a.b', 'a.b', 2, [#a, #b]); |
| 60 expectPath('a. b', 'a.b', 2, [#a, #b]); |
| 61 expectPath('a .b', 'a.b', 2, [#a, #b]); |
| 62 expectPath('a . b', 'a.b', 2, [#a, #b]); |
| 63 expectPath(' a . b ', 'a.b', 2, [#a, #b]); |
| 64 expectPath('a[0]', 'a[0]', 2, [#a, 0]); |
| 65 expectPath('a [0]', 'a[0]', 2, [#a, 0]); |
| 66 expectPath('a[0][1]', 'a[0][1]', 3, [#a, 0, 1]); |
| 67 expectPath('a [ 0 ] [ 1 ] ', 'a[0][1]', 3, [#a, 0, 1]); |
| 68 expectPath('[1234567890] ', '[1234567890]', 1, [1234567890]); |
| 69 expectPath(' [1234567890] ', '[1234567890]', 1, [1234567890]); |
| 70 expectPath('opt0', 'opt0', 1, [#opt0]); |
| 71 // Dart note: Modified to avoid a private Dart symbol: |
| 72 expectPath(r'$foo.$bar.baz_', r'$foo.$bar.baz_', 3, |
| 73 [#$foo, #$bar, #baz_]); |
| 74 // Dart note: this test is different because we treat ["baz"] always as a |
| 75 // indexing operation. |
| 76 expectPath('foo["baz"]', 'foo.baz', 2, [#foo, #baz]); |
| 77 expectPath('foo["b\\"az"]', 'foo["b\\"az"]', 2, [#foo, 'b"az']); |
| 78 expectPath("foo['b\\'az']", 'foo["b\'az"]', 2, [#foo, "b'az"]); |
| 79 expectPath([#a, #b], 'a.b', 2, [#a, #b]); |
| 80 expectPath([], '', 0, []); |
| 81 |
| 82 expectPath('.', '<invalid path>', 0); |
| 83 expectPath(' . ', '<invalid path>', 0); |
| 84 expectPath('..', '<invalid path>', 0); |
| 85 expectPath('a[4', '<invalid path>', 0); |
| 86 expectPath('a.b.', '<invalid path>', 0); |
| 87 expectPath('a,b', '<invalid path>', 0); |
| 88 expectPath('a["foo]', '<invalid path>', 0); |
| 89 expectPath('[0x04]', '<invalid path>', 0); |
| 90 expectPath('[0foo]', '<invalid path>', 0); |
| 91 expectPath('[foo-bar]', '<invalid path>', 0); |
| 92 expectPath('foo-bar', '<invalid path>', 0); |
| 93 expectPath('42', '<invalid path>', 0); |
| 94 expectPath('a[04]', '<invalid path>', 0); |
| 95 expectPath(' a [ 04 ]', '<invalid path>', 0); |
| 96 expectPath(' 42 ', '<invalid path>', 0); |
| 97 expectPath('foo["bar]', '<invalid path>', 0); |
| 98 expectPath("foo['bar]", '<invalid path>', 0); |
| 99 }); |
| 100 |
| 101 test('objects with toString are not supported', () { |
| 102 // Dart note: this was intentionally not ported. See path_observer.dart. |
| 103 expect(() => new PropertyPath([new Foo('a'), new Foo('b')]), throws); |
| 104 }); |
| 105 |
| 106 test('invalid path returns null value', () { |
| 107 var path = new PropertyPath('a b'); |
| 108 expect(path.isValid, isFalse); |
| 109 expect(path.getValueFrom({'a': {'b': 2}}), isNull); |
| 110 }); |
| 111 |
| 112 |
| 113 test('caching and ==', () { |
| 114 var start = new PropertyPath('abc[0]'); |
| 115 for (int i = 1; i <= 100; i++) { |
| 116 expect(identical(new PropertyPath('abc[0]'), start), true, |
| 117 reason: 'should return identical path'); |
| 118 |
| 119 var p = new PropertyPath('abc[$i]'); |
| 120 expect(identical(p, start), false, |
| 121 reason: 'different paths should not be merged'); |
| 122 } |
| 123 var end = new PropertyPath('abc[0]'); |
| 124 expect(identical(end, start), false, |
| 125 reason: 'first entry expired'); |
| 126 expect(end, start, reason: 'different instances are equal'); |
| 127 }); |
| 128 |
| 129 test('hashCode equal', () { |
| 130 var a = new PropertyPath([#foo, 2, #bar]); |
| 131 var b = new PropertyPath('foo[2].bar'); |
| 132 expect(identical(a, b), false, reason: 'only strings cached'); |
| 133 expect(a, b, reason: 'same paths are equal'); |
| 134 expect(a.hashCode, b.hashCode, reason: 'equal hashCodes'); |
| 135 }); |
| 136 |
| 137 test('hashCode not equal', () { |
| 138 expect(2.hashCode, isNot(3.hashCode), |
| 139 reason: 'test depends on 2 and 3 having different hashcodes'); |
| 140 |
| 141 var a = new PropertyPath([2]); |
| 142 var b = new PropertyPath([3]); |
| 143 expect(a, isNot(b), reason: 'different paths'); |
| 144 expect(a.hashCode, isNot(b.hashCode), reason: 'different hashCodes'); |
| 145 }); |
| 146 }); |
| 147 |
| 148 group('CompoundObserver', compoundObserverTests); |
| 149 }); |
| 150 |
| 151 observePathTests() { |
| 152 test('Degenerate Values', () { |
| 153 expect(new PathObserver(null, '').value, null); |
| 154 expect(new PathObserver(123, '').value, 123); |
| 155 expect(() => new PathObserver(123, 'foo.bar.baz').value, _throwsNSM('foo')); |
| 156 |
| 157 // shouldn't throw: |
| 158 new PathObserver(123, '')..open((_) {})..close(); |
| 159 new PropertyPath('').setValueFrom(null, null); |
| 160 new PropertyPath('').setValueFrom(123, 42); |
| 161 expect(() => new PropertyPath('foo.bar.baz').setValueFrom(123, 42), |
| 162 _throwsNSM('foo')); |
| 163 var foo = {}; |
| 164 expect(new PathObserver(foo, '').value, foo); |
| 165 |
| 166 foo = new Object(); |
| 167 expect(new PathObserver(foo, '').value, foo); |
| 168 |
| 169 expect(new PathObserver(foo, 'a/3!').value, null); |
| 170 }); |
| 171 |
| 172 test('get value at path ObservableBox', () { |
| 173 var obj = new ObservableBox(new ObservableBox(new ObservableBox(1))); |
| 174 |
| 175 expect(new PathObserver(obj, '').value, obj); |
| 176 expect(new PathObserver(obj, 'value').value, obj.value); |
| 177 expect(new PathObserver(obj, 'value.value').value, obj.value.value); |
| 178 expect(new PathObserver(obj, 'value.value.value').value, 1); |
| 179 |
| 180 obj.value.value.value = 2; |
| 181 expect(new PathObserver(obj, 'value.value.value').value, 2); |
| 182 |
| 183 obj.value.value = new ObservableBox(3); |
| 184 expect(new PathObserver(obj, 'value.value.value').value, 3); |
| 185 |
| 186 obj.value = new ObservableBox(4); |
| 187 expect(() => new PathObserver(obj, 'value.value.value').value, |
| 188 _throwsNSM('value')); |
| 189 expect(new PathObserver(obj, 'value.value').value, 4); |
| 190 }); |
| 191 |
| 192 |
| 193 test('get value at path ObservableMap', () { |
| 194 var obj = toObservable({'a': {'b': {'c': 1}}}); |
| 195 |
| 196 expect(new PathObserver(obj, '').value, obj); |
| 197 expect(new PathObserver(obj, 'a').value, obj['a']); |
| 198 expect(new PathObserver(obj, 'a.b').value, obj['a']['b']); |
| 199 expect(new PathObserver(obj, 'a.b.c').value, 1); |
| 200 |
| 201 obj['a']['b']['c'] = 2; |
| 202 expect(new PathObserver(obj, 'a.b.c').value, 2); |
| 203 |
| 204 obj['a']['b'] = toObservable({'c': 3}); |
| 205 expect(new PathObserver(obj, 'a.b.c').value, 3); |
| 206 |
| 207 obj['a'] = toObservable({'b': 4}); |
| 208 expect(() => new PathObserver(obj, 'a.b.c').value, _throwsNSM('c')); |
| 209 expect(new PathObserver(obj, 'a.b').value, 4); |
| 210 }); |
| 211 |
| 212 test('set value at path', () { |
| 213 var obj = toObservable({}); |
| 214 new PropertyPath('foo').setValueFrom(obj, 3); |
| 215 expect(obj['foo'], 3); |
| 216 |
| 217 var bar = toObservable({ 'baz': 3 }); |
| 218 new PropertyPath('bar').setValueFrom(obj, bar); |
| 219 expect(obj['bar'], bar); |
| 220 |
| 221 expect(() => new PropertyPath('bar.baz.bat').setValueFrom(obj, 'not here'), |
| 222 _throwsNSM('bat=')); |
| 223 expect(() => new PathObserver(obj, 'bar.baz.bat').value, _throwsNSM('bat')); |
| 224 }); |
| 225 |
| 226 test('set value back to same', () { |
| 227 var obj = toObservable({}); |
| 228 var path = new PathObserver(obj, 'foo'); |
| 229 var values = []; |
| 230 path.open((x) { |
| 231 expect(x, path.value, reason: 'callback should get current value'); |
| 232 values.add(x); |
| 233 }); |
| 234 |
| 235 path.value = 3; |
| 236 expect(obj['foo'], 3); |
| 237 expect(path.value, 3); |
| 238 |
| 239 new PropertyPath('foo').setValueFrom(obj, 2); |
| 240 return new Future(() { |
| 241 expect(path.value, 2); |
| 242 expect(new PathObserver(obj, 'foo').value, 2); |
| 243 |
| 244 new PropertyPath('foo').setValueFrom(obj, 3); |
| 245 }).then(newMicrotask).then((_) { |
| 246 expect(path.value, 3); |
| 247 |
| 248 }).then(newMicrotask).then((_) { |
| 249 expect(values, [2, 3]); |
| 250 }); |
| 251 }); |
| 252 |
| 253 test('Observe and Unobserve - Paths', () { |
| 254 var arr = toObservable({}); |
| 255 |
| 256 arr['foo'] = 'bar'; |
| 257 var fooValues = []; |
| 258 var fooPath = new PathObserver(arr, 'foo'); |
| 259 fooPath.open(fooValues.add); |
| 260 arr['foo'] = 'baz'; |
| 261 arr['bat'] = 'bag'; |
| 262 var batValues = []; |
| 263 var batPath = new PathObserver(arr, 'bat'); |
| 264 batPath.open(batValues.add); |
| 265 |
| 266 return new Future(() { |
| 267 expect(fooValues, ['baz']); |
| 268 expect(batValues, []); |
| 269 |
| 270 arr['foo'] = 'bar'; |
| 271 fooPath.close(); |
| 272 arr['bat'] = 'boo'; |
| 273 batPath.close(); |
| 274 arr['bat'] = 'boot'; |
| 275 |
| 276 }).then(newMicrotask).then((_) { |
| 277 expect(fooValues, ['baz']); |
| 278 expect(batValues, []); |
| 279 }); |
| 280 }); |
| 281 |
| 282 test('Path Value With Indices', () { |
| 283 var model = toObservable([]); |
| 284 var path = new PathObserver(model, '[0]'); |
| 285 path.open(expectAsync((x) { |
| 286 expect(path.value, 123); |
| 287 expect(x, 123); |
| 288 })); |
| 289 model.add(123); |
| 290 }); |
| 291 |
| 292 group('ObservableList', () { |
| 293 test('isNotEmpty', () { |
| 294 var model = new ObservableList(); |
| 295 var path = new PathObserver(model, 'isNotEmpty'); |
| 296 expect(path.value, false); |
| 297 |
| 298 path.open(expectAsync((_) { |
| 299 expect(path.value, true); |
| 300 })); |
| 301 model.add(123); |
| 302 }); |
| 303 |
| 304 test('isEmpty', () { |
| 305 var model = new ObservableList(); |
| 306 var path = new PathObserver(model, 'isEmpty'); |
| 307 expect(path.value, true); |
| 308 |
| 309 path.open(expectAsync((_) { |
| 310 expect(path.value, false); |
| 311 })); |
| 312 model.add(123); |
| 313 }); |
| 314 }); |
| 315 |
| 316 for (var createModel in [() => new TestModel(), () => new WatcherModel()]) { |
| 317 test('Path Observation - ${createModel().runtimeType}', () { |
| 318 var model = createModel()..a = |
| 319 (createModel()..b = (createModel()..c = 'hello, world')); |
| 320 |
| 321 var path = new PathObserver(model, 'a.b.c'); |
| 322 var lastValue = null; |
| 323 var errorSeen = false; |
| 324 runZoned(() { |
| 325 path.open((x) { lastValue = x; }); |
| 326 }, onError: (e) { |
| 327 expect(e, _isNoSuchMethodOf('c')); |
| 328 errorSeen = true; |
| 329 }); |
| 330 |
| 331 model.a.b.c = 'hello, mom'; |
| 332 |
| 333 expect(lastValue, null); |
| 334 return new Future(() { |
| 335 expect(lastValue, 'hello, mom'); |
| 336 |
| 337 model.a.b = createModel()..c = 'hello, dad'; |
| 338 }).then(newMicrotask).then((_) { |
| 339 expect(lastValue, 'hello, dad'); |
| 340 |
| 341 model.a = createModel()..b = |
| 342 (createModel()..c = 'hello, you'); |
| 343 }).then(newMicrotask).then((_) { |
| 344 expect(lastValue, 'hello, you'); |
| 345 |
| 346 model.a.b = 1; |
| 347 expect(errorSeen, isFalse); |
| 348 }).then(newMicrotask).then((_) { |
| 349 expect(errorSeen, isTrue); |
| 350 expect(lastValue, 'hello, you'); |
| 351 |
| 352 // Stop observing |
| 353 path.close(); |
| 354 |
| 355 model.a.b = createModel()..c = 'hello, back again -- but not observing'; |
| 356 }).then(newMicrotask).then((_) { |
| 357 expect(lastValue, 'hello, you'); |
| 358 |
| 359 // Resume observing |
| 360 new PathObserver(model, 'a.b.c').open((x) { lastValue = x; }); |
| 361 |
| 362 model.a.b.c = 'hello. Back for reals'; |
| 363 }).then(newMicrotask).then((_) { |
| 364 expect(lastValue, 'hello. Back for reals'); |
| 365 }); |
| 366 }); |
| 367 } |
| 368 |
| 369 test('observe map', () { |
| 370 var model = toObservable({'a': 1}); |
| 371 var path = new PathObserver(model, 'a'); |
| 372 |
| 373 var values = [path.value]; |
| 374 path.open(values.add); |
| 375 expect(values, [1]); |
| 376 |
| 377 model['a'] = 2; |
| 378 return new Future(() { |
| 379 expect(values, [1, 2]); |
| 380 |
| 381 path.close(); |
| 382 model['a'] = 3; |
| 383 }).then(newMicrotask).then((_) { |
| 384 expect(values, [1, 2]); |
| 385 }); |
| 386 }); |
| 387 |
| 388 test('errors thrown from getter/setter', () { |
| 389 var model = new ObjectWithErrors(); |
| 390 var observer = new PathObserver(model, 'foo'); |
| 391 |
| 392 expect(() => observer.value, _throwsNSM('bar')); |
| 393 expect(model.getFooCalled, 1); |
| 394 |
| 395 expect(() { observer.value = 123; }, _throwsNSM('bar=')); |
| 396 expect(model.setFooCalled, [123]); |
| 397 }); |
| 398 |
| 399 test('object with noSuchMethod', () { |
| 400 var model = new NoSuchMethodModel(); |
| 401 var observer = new PathObserver(model, 'foo'); |
| 402 |
| 403 expect(observer.value, 42); |
| 404 observer.value = 'hi'; |
| 405 expect(model._foo, 'hi'); |
| 406 expect(observer.value, 'hi'); |
| 407 |
| 408 expect(model.log, [#foo, const Symbol('foo='), #foo]); |
| 409 |
| 410 // These shouldn't throw |
| 411 observer = new PathObserver(model, 'bar'); |
| 412 expect(observer.value, null, reason: 'path not found'); |
| 413 observer.value = 42; |
| 414 expect(observer.value, null, reason: 'path not found'); |
| 415 }); |
| 416 |
| 417 test('object with indexer', () { |
| 418 var model = new IndexerModel(); |
| 419 var observer = new PathObserver(model, 'foo'); |
| 420 |
| 421 expect(observer.value, 42); |
| 422 expect(model.log, ['[] foo']); |
| 423 model.log.clear(); |
| 424 |
| 425 observer.value = 'hi'; |
| 426 expect(model.log, ['[]= foo hi']); |
| 427 expect(model._foo, 'hi'); |
| 428 |
| 429 expect(observer.value, 'hi'); |
| 430 |
| 431 // These shouldn't throw |
| 432 model.log.clear(); |
| 433 observer = new PathObserver(model, 'bar'); |
| 434 expect(observer.value, null, reason: 'path not found'); |
| 435 expect(model.log, ['[] bar']); |
| 436 model.log.clear(); |
| 437 |
| 438 observer.value = 42; |
| 439 expect(model.log, ['[]= bar 42']); |
| 440 model.log.clear(); |
| 441 }); |
| 442 |
| 443 test('regression for TemplateBinding#161', () { |
| 444 var model = toObservable({'obj': toObservable({'bar': false})}); |
| 445 var ob1 = new PathObserver(model, 'obj.bar'); |
| 446 var called = false; |
| 447 ob1.open(() { called = true; }); |
| 448 |
| 449 var obj2 = new PathObserver(model, 'obj'); |
| 450 obj2.open(() { model['obj']['bar'] = true; }); |
| 451 |
| 452 model['obj'] = toObservable({ 'obj': 'obj' }); |
| 453 |
| 454 return new Future(() {}) |
| 455 .then((_) => expect(called, true)); |
| 456 }); |
| 457 } |
| 458 |
| 459 compoundObserverTests() { |
| 460 var model; |
| 461 var observer; |
| 462 bool called; |
| 463 var newValues; |
| 464 var oldValues; |
| 465 var observed; |
| 466 |
| 467 setUp(() { |
| 468 model = new TestModel(1, 2, 3); |
| 469 called = false; |
| 470 }); |
| 471 |
| 472 callback(a, b, c) { |
| 473 called = true; |
| 474 newValues = a; |
| 475 oldValues = b; |
| 476 observed = c; |
| 477 } |
| 478 |
| 479 reset() { |
| 480 called = false; |
| 481 newValues = null; |
| 482 oldValues = null; |
| 483 observed = null; |
| 484 } |
| 485 |
| 486 expectNoChanges() { |
| 487 observer.deliver(); |
| 488 expect(called, isFalse); |
| 489 expect(newValues, isNull); |
| 490 expect(oldValues, isNull); |
| 491 expect(observed, isNull); |
| 492 } |
| 493 |
| 494 expectCompoundPathChanges(expectedNewValues, |
| 495 expectedOldValues, expectedObserved, {deliver: true}) { |
| 496 if (deliver) observer.deliver(); |
| 497 expect(called, isTrue); |
| 498 |
| 499 expect(newValues, expectedNewValues); |
| 500 var oldValuesAsMap = {}; |
| 501 for (int i = 0; i < expectedOldValues.length; i++) { |
| 502 if (expectedOldValues[i] != null) { |
| 503 oldValuesAsMap[i] = expectedOldValues[i]; |
| 504 } |
| 505 } |
| 506 expect(oldValues, oldValuesAsMap); |
| 507 expect(observed, expectedObserved); |
| 508 |
| 509 reset(); |
| 510 } |
| 511 |
| 512 tearDown(() { |
| 513 observer.close(); |
| 514 reset(); |
| 515 }); |
| 516 |
| 517 _path(s) => new PropertyPath(s); |
| 518 |
| 519 test('simple', () { |
| 520 observer = new CompoundObserver(); |
| 521 observer.addPath(model, 'a'); |
| 522 observer.addPath(model, 'b'); |
| 523 observer.addPath(model, _path('c')); |
| 524 observer.open(callback); |
| 525 expectNoChanges(); |
| 526 |
| 527 var expectedObs = [model, _path('a'), model, _path('b'), model, _path('c')]; |
| 528 model.a = -10; |
| 529 model.b = 20; |
| 530 model.c = 30; |
| 531 expectCompoundPathChanges([-10, 20, 30], [1, 2, 3], expectedObs); |
| 532 |
| 533 model.a = 'a'; |
| 534 model.c = 'c'; |
| 535 expectCompoundPathChanges(['a', 20, 'c'], [-10, null, 30], expectedObs); |
| 536 |
| 537 model.a = 2; |
| 538 model.b = 3; |
| 539 model.c = 4; |
| 540 expectCompoundPathChanges([2, 3, 4], ['a', 20, 'c'], expectedObs); |
| 541 |
| 542 model.a = 'z'; |
| 543 model.b = 'y'; |
| 544 model.c = 'x'; |
| 545 expect(observer.value, ['z', 'y', 'x']); |
| 546 expectNoChanges(); |
| 547 |
| 548 expect(model.a, 'z'); |
| 549 expect(model.b, 'y'); |
| 550 expect(model.c, 'x'); |
| 551 expectNoChanges(); |
| 552 }); |
| 553 |
| 554 test('reportChangesOnOpen', () { |
| 555 observer = new CompoundObserver(true); |
| 556 observer.addPath(model, 'a'); |
| 557 observer.addPath(model, 'b'); |
| 558 observer.addPath(model, _path('c')); |
| 559 |
| 560 model.a = -10; |
| 561 model.b = 20; |
| 562 observer.open(callback); |
| 563 var expectedObs = [model, _path('a'), model, _path('b'), model, _path('c')]; |
| 564 expectCompoundPathChanges([-10, 20, 3], [1, 2, null], expectedObs, |
| 565 deliver: false); |
| 566 }); |
| 567 |
| 568 test('All Observers', () { |
| 569 observer = new CompoundObserver(); |
| 570 var pathObserver1 = new PathObserver(model, 'a'); |
| 571 var pathObserver2 = new PathObserver(model, 'b'); |
| 572 var pathObserver3 = new PathObserver(model, _path('c')); |
| 573 |
| 574 observer.addObserver(pathObserver1); |
| 575 observer.addObserver(pathObserver2); |
| 576 observer.addObserver(pathObserver3); |
| 577 observer.open(callback); |
| 578 |
| 579 var expectedObs = [observerSentinelForTesting, pathObserver1, |
| 580 observerSentinelForTesting, pathObserver2, |
| 581 observerSentinelForTesting, pathObserver3]; |
| 582 model.a = -10; |
| 583 model.b = 20; |
| 584 model.c = 30; |
| 585 expectCompoundPathChanges([-10, 20, 30], [1, 2, 3], expectedObs); |
| 586 |
| 587 model.a = 'a'; |
| 588 model.c = 'c'; |
| 589 expectCompoundPathChanges(['a', 20, 'c'], [-10, null, 30], expectedObs); |
| 590 }); |
| 591 |
| 592 test('Degenerate Values', () { |
| 593 observer = new CompoundObserver(); |
| 594 observer.addPath(model, '.'); // invalid path |
| 595 observer.addPath('obj-value', ''); // empty path |
| 596 // Dart note: we don't port these two tests because in Dart we produce |
| 597 // exceptions for these invalid paths. |
| 598 // observer.addPath(model, 'foo'); // unreachable |
| 599 // observer.addPath(3, 'bar'); // non-object with non-empty path |
| 600 var values = observer.open(callback); |
| 601 expect(values.length, 2); |
| 602 expect(values[0], null); |
| 603 expect(values[1], 'obj-value'); |
| 604 observer.close(); |
| 605 }); |
| 606 |
| 607 test('Heterogeneous', () { |
| 608 model.c = null; |
| 609 var otherModel = new TestModel(null, null, 3); |
| 610 |
| 611 twice(value) => value * 2; |
| 612 half(value) => value ~/ 2; |
| 613 |
| 614 var compound = new CompoundObserver(); |
| 615 compound.addPath(model, 'a'); |
| 616 compound.addObserver(new ObserverTransform(new PathObserver(model, 'b'), |
| 617 twice, setValue: half)); |
| 618 compound.addObserver(new PathObserver(otherModel, 'c')); |
| 619 |
| 620 combine(values) => values[0] + values[1] + values[2]; |
| 621 observer = new ObserverTransform(compound, combine); |
| 622 |
| 623 var newValue; |
| 624 transformCallback(v) { |
| 625 newValue = v; |
| 626 called = true; |
| 627 } |
| 628 expect(observer.open(transformCallback), 8); |
| 629 |
| 630 model.a = 2; |
| 631 model.b = 4; |
| 632 observer.deliver(); |
| 633 expect(called, isTrue); |
| 634 expect(newValue, 13); |
| 635 called = false; |
| 636 |
| 637 model.b = 10; |
| 638 otherModel.c = 5; |
| 639 observer.deliver(); |
| 640 expect(called, isTrue); |
| 641 expect(newValue, 27); |
| 642 called = false; |
| 643 |
| 644 model.a = 20; |
| 645 model.b = 1; |
| 646 otherModel.c = 5; |
| 647 observer.deliver(); |
| 648 expect(called, isFalse); |
| 649 expect(newValue, 27); |
| 650 }); |
| 651 } |
| 652 |
| 653 /// A matcher that checks that a closure throws a NoSuchMethodError matching the |
| 654 /// given [name]. |
| 655 _throwsNSM(String name) => throwsA(_isNoSuchMethodOf(name)); |
| 656 |
| 657 /// A matcher that checkes whether an exception is a NoSuchMethodError matching |
| 658 /// the given [name]. |
| 659 _isNoSuchMethodOf(String name) => predicate((e) => |
| 660 e is NoSuchMethodError && |
| 661 // Dart2js and VM error messages are a bit different, but they both contain |
| 662 // the name. |
| 663 ('$e'.contains("'$name'") || // VM error |
| 664 '$e'.contains('\'Symbol("$name")\''))); // dart2js error |
| 665 |
| 666 class ObjectWithErrors { |
| 667 int getFooCalled = 0; |
| 668 List setFooCalled = []; |
| 669 @reflectable get foo { |
| 670 getFooCalled++; |
| 671 (this as dynamic).bar; |
| 672 } |
| 673 @reflectable set foo(value) { |
| 674 setFooCalled.add(value); |
| 675 (this as dynamic).bar = value; |
| 676 } |
| 677 } |
| 678 |
| 679 class NoSuchMethodModel { |
| 680 var _foo = 42; |
| 681 List log = []; |
| 682 |
| 683 // TODO(ahe): Remove @reflectable from here (once either of |
| 684 // http://dartbug.com/15408 or http://dartbug.com/15409 are fixed). |
| 685 @reflectable noSuchMethod(Invocation invocation) { |
| 686 final name = invocation.memberName; |
| 687 log.add(name); |
| 688 if (name == #foo && invocation.isGetter) return _foo; |
| 689 if (name == const Symbol('foo=')) { |
| 690 _foo = invocation.positionalArguments[0]; |
| 691 return null; |
| 692 } |
| 693 return super.noSuchMethod(invocation); |
| 694 } |
| 695 } |
| 696 |
| 697 class IndexerModel implements Indexable<String, dynamic> { |
| 698 var _foo = 42; |
| 699 List log = []; |
| 700 |
| 701 operator [](index) { |
| 702 log.add('[] $index'); |
| 703 if (index == 'foo') return _foo; |
| 704 } |
| 705 |
| 706 operator []=(index, value) { |
| 707 log.add('[]= $index $value'); |
| 708 if (index == 'foo') _foo = value; |
| 709 } |
| 710 } |
| 711 |
| 712 @reflectable |
| 713 class TestModel extends ChangeNotifier { |
| 714 var _a, _b, _c; |
| 715 |
| 716 TestModel([this._a, this._b, this._c]); |
| 717 |
| 718 get a => _a; |
| 719 |
| 720 void set a(newValue) { |
| 721 _a = notifyPropertyChange(#a, _a, newValue); |
| 722 } |
| 723 |
| 724 get b => _b; |
| 725 |
| 726 void set b(newValue) { |
| 727 _b = notifyPropertyChange(#b, _b, newValue); |
| 728 } |
| 729 |
| 730 get c => _c; |
| 731 |
| 732 void set c(newValue) { |
| 733 _c = notifyPropertyChange(#c, _c, newValue); |
| 734 } |
| 735 } |
| 736 |
| 737 class WatcherModel extends Observable { |
| 738 // TODO(jmesserly): dart2js does not let these be on the same line: |
| 739 // @observable var a, b, c; |
| 740 @observable var a; |
| 741 @observable var b; |
| 742 @observable var c; |
| 743 |
| 744 WatcherModel(); |
| 745 } |
| 746 |
| 747 class Foo { |
| 748 var value; |
| 749 Foo(this.value); |
| 750 String toString() => 'Foo$value'; |
| 751 } |
OLD | NEW |