OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2012, 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 /** Tests for some of the utility helper functions used by the compiler. */ |
| 6 library observe_test; |
| 7 |
| 8 import 'dart:collection' show LinkedHashMap; |
| 9 import 'package:unittest/compact_vm_config.dart'; |
| 10 import 'package:unittest/unittest.dart'; |
| 11 import 'package:web_ui/observe.dart'; |
| 12 import 'package:web_ui/src/utils.dart' show setImmediate; |
| 13 |
| 14 main() { |
| 15 useCompactVMConfiguration(); |
| 16 |
| 17 group('TestObservable', () { |
| 18 test('no observers', () { |
| 19 var t = new TestObservable<int>(123); |
| 20 expect(t.value, 123); |
| 21 expect(t.rawValue, 123); |
| 22 t.value = 42; |
| 23 expect(t.value, 42); |
| 24 expect(t.rawValue, 42); |
| 25 expect(t.observers, null); |
| 26 }); |
| 27 |
| 28 test('observe', () { |
| 29 var t = new TestObservable<int>(123); |
| 30 int called = 0; |
| 31 observe(() => t.value, expectAsync1((ChangeNotification n) { |
| 32 called++; |
| 33 expect(n.oldValue, 123); |
| 34 expect(n.newValue, 42); |
| 35 })); |
| 36 t.value = 41; |
| 37 t.value = 42; |
| 38 expect(called, 0, reason: 'changes delived async'); |
| 39 }); |
| 40 |
| 41 test('observe multiple changes', () { |
| 42 var t = new TestObservable<int>(123); |
| 43 observe(() => t.value, expectAsync1((ChangeNotification n) { |
| 44 if (n.oldValue == 123) { |
| 45 expect(n.newValue, 42); |
| 46 // Cause another change |
| 47 t.value = 777; |
| 48 } else { |
| 49 expect(n.oldValue, 42); |
| 50 expect(n.newValue, 777); |
| 51 } |
| 52 }, count: 2)); |
| 53 t.value = 42; |
| 54 }); |
| 55 |
| 56 test('multiple observers', () { |
| 57 var t = new TestObservable<int>(123); |
| 58 observe(() => t.value, expectAsync1((ChangeNotification n) { |
| 59 expect(n.oldValue, 123); |
| 60 expect(n.newValue, 42); |
| 61 })); |
| 62 observe(() => t.value + 1, expectAsync1((ChangeNotification n) { |
| 63 expect(n.oldValue, 124); |
| 64 expect(n.newValue, 43); |
| 65 })); |
| 66 t.value = 41; |
| 67 t.value = 42; |
| 68 }); |
| 69 |
| 70 test('deliverChangesSync', () { |
| 71 var t = new TestObservable<int>(123); |
| 72 var notifications = []; |
| 73 observe(() => t.value, notifications.add); |
| 74 t.value = 41; |
| 75 t.value = 42; |
| 76 expect(notifications, [], reason: 'changes delived async'); |
| 77 |
| 78 deliverChangesSync(); |
| 79 expect(notifications, [_change(123, 42)]); |
| 80 t.value = 777; |
| 81 expect(notifications.length, 1, reason: 'changes delived async'); |
| 82 |
| 83 deliverChangesSync(); |
| 84 expect(notifications, [_change(123, 42), _change(42, 777)]); |
| 85 |
| 86 // Has no effect if there are no changes |
| 87 deliverChangesSync(); |
| 88 expect(notifications, [_change(123, 42), _change(42, 777)]); |
| 89 }); |
| 90 |
| 91 test('unobserve', () { |
| 92 var t = new TestObservable<int>(123); |
| 93 ChangeUnobserver unobserve; |
| 94 unobserve = observe(() => t.value, expectAsync1((n) { |
| 95 expect(n.oldValue, 123); |
| 96 expect(n.newValue, 42); |
| 97 unobserve(); |
| 98 t.value = 777; |
| 99 })); |
| 100 t.value = 42; |
| 101 }); |
| 102 |
| 103 test('observers fired in order', () { |
| 104 var t = new TestObservable<int>(123); |
| 105 int expectOldValue = 123; |
| 106 int expectNewValue = 42; |
| 107 observe(() => t.value, expectAsync1((n) { |
| 108 expect(n.oldValue, expectOldValue); |
| 109 expect(n.newValue, expectNewValue); |
| 110 |
| 111 // The second observer will see this change already, and only be called |
| 112 // once. However we'll be called a second time. |
| 113 t.value = 777; |
| 114 expectNewValue = 777; |
| 115 expectOldValue = 42; |
| 116 }, count: 2)); |
| 117 |
| 118 observe(() => t.value + 1000, expectAsync1((n) { |
| 119 expect(n.oldValue, 1123); |
| 120 expect(n.newValue, 1777); |
| 121 })); |
| 122 |
| 123 // Make the initial change |
| 124 t.value = 42; |
| 125 }); |
| 126 |
| 127 test('unobserve one of two observers', () { |
| 128 var t = new TestObservable<int>(123); |
| 129 ChangeUnobserver unobserve; |
| 130 unobserve = observe(() => t.value, expectAsync1((n) { |
| 131 expect(n.oldValue, 123); |
| 132 expect(n.newValue, 42); |
| 133 |
| 134 // This will not affect the other observer, so it still gets the event. |
| 135 unobserve(); |
| 136 setImmediate(() => t.value = 777); |
| 137 })); |
| 138 int count = 0; |
| 139 observe(() => t.value + 1000, expectAsync1((n) { |
| 140 if (++count == 1) { |
| 141 expect(n.oldValue, 1123); |
| 142 expect(n.newValue, 1042); |
| 143 } else { |
| 144 expect(n.oldValue, 1042); |
| 145 expect(n.newValue, 1777); |
| 146 } |
| 147 }, count: 2)); |
| 148 |
| 149 // Make the initial change |
| 150 t.value = 42; |
| 151 }); |
| 152 |
| 153 test('notifyRead in getter', () { |
| 154 var t = new TestObservable<int>(123); |
| 155 |
| 156 observe(() { |
| 157 expect(observeReads, true); |
| 158 expect(t.observers, null); |
| 159 return t.value; |
| 160 }, (n) {}); |
| 161 |
| 162 expect(observeReads, false); |
| 163 expect(t.observers, isNotNull); |
| 164 }); |
| 165 |
| 166 test('notifyWrite in setter', () { |
| 167 var t = new TestObservable<int>(123); |
| 168 observe(() => t.value, (n) {}); |
| 169 |
| 170 t.value = 42; |
| 171 expect(observeReads, false); |
| 172 expect(t.observers, null); |
| 173 |
| 174 // This will re-observe the expression. |
| 175 deliverChangesSync(); |
| 176 |
| 177 expect(observeReads, false); |
| 178 expect(t.observers, isNotNull); |
| 179 }); |
| 180 |
| 181 test('observe conditional async', () { |
| 182 var t = new TestObservable<bool>(false); |
| 183 var a = new TestObservable<int>(123); |
| 184 var b = new TestObservable<String>('hi'); |
| 185 |
| 186 int count = 0; |
| 187 var oldValue = 'hi'; |
| 188 observe(() => t.value ? a.value : b.value, expectAsync1((n) { |
| 189 expect(n.oldValue, oldValue); |
| 190 oldValue = t.value ? a.value : b.value; |
| 191 expect(n.newValue, oldValue); |
| 192 |
| 193 switch (++count) { |
| 194 case 1: |
| 195 // We are observing "a", change it |
| 196 a.value = 42; |
| 197 break; |
| 198 case 2: |
| 199 // Switch to observing "b" |
| 200 t.value = false; |
| 201 break; |
| 202 case 3: |
| 203 // Change "a", this should have no effect and will not fire a 4th |
| 204 // change event. |
| 205 a.value = 777; |
| 206 expect(a.observers, null); |
| 207 expect(b.observers, isNotNull); |
| 208 break; |
| 209 default: |
| 210 // Should not be able to reach this because of the "count" argument |
| 211 // to expectAsync1 |
| 212 throw new StateError('unreachable'); |
| 213 } |
| 214 }, count: 3)); |
| 215 |
| 216 expect(t.observers, isNotNull); |
| 217 expect(a.observers, null); |
| 218 expect(b.observers, isNotNull); |
| 219 |
| 220 // Start off by changing "t" to true. |
| 221 t.value = true; |
| 222 }); |
| 223 |
| 224 test('change limit set to null (unbounded)', () { |
| 225 const BIG_LIMIT = 1000; |
| 226 expect(circularNotifyLimit, lessThan(BIG_LIMIT)); |
| 227 |
| 228 int oldLimit = circularNotifyLimit; |
| 229 circularNotifyLimit = null; |
| 230 try { |
| 231 var x = new TestObservable(false); |
| 232 var y = new TestObservable(false); |
| 233 |
| 234 int xCount = 0, yCount = 0; |
| 235 int limit = BIG_LIMIT; |
| 236 observe(() => x.value, (n) { |
| 237 if (++xCount < limit) y.value = x.value; |
| 238 }); |
| 239 observe(() => y.value, (n) { |
| 240 if (++yCount < limit) x.value = !y.value; |
| 241 }); |
| 242 |
| 243 // Kick off the cascading changes |
| 244 x.value = true; |
| 245 |
| 246 deliverChangesSync(); |
| 247 |
| 248 expect(xCount, limit); |
| 249 expect(yCount, limit - 1); |
| 250 } finally { |
| 251 circularNotifyLimit = oldLimit; |
| 252 } |
| 253 }); |
| 254 }); |
| 255 |
| 256 group('ObservableReference', () { |
| 257 test('observe conditional sync', () { |
| 258 var t = new ObservableReference<bool>(false); |
| 259 var a = new ObservableReference<int>(123); |
| 260 var b = new ObservableReference<String>('hi'); |
| 261 |
| 262 var notifications = []; |
| 263 observe(() => t.value ? a.value : b.value, notifications.add); |
| 264 |
| 265 // Start off by changing "t" to true, so we evaluate "a". |
| 266 t.value = true; |
| 267 deliverChangesSync(); |
| 268 |
| 269 // This changes "a" which we should be observing. |
| 270 a.value = 42; |
| 271 deliverChangesSync(); |
| 272 |
| 273 // This has no effect because we aren't using "b" yet. |
| 274 b.value = 'universe'; |
| 275 deliverChangesSync(); |
| 276 |
| 277 // Switch to use "b". |
| 278 t.value = false; |
| 279 deliverChangesSync(); |
| 280 |
| 281 // This has no effect because we aren't using "a" anymore. |
| 282 a.value = 777; |
| 283 deliverChangesSync(); |
| 284 |
| 285 expect(notifications, [ |
| 286 _change('hi', 123), |
| 287 _change(123, 42), |
| 288 _change(42, 'universe')]); |
| 289 }); |
| 290 }); |
| 291 |
| 292 |
| 293 group('ObservableList', () { |
| 294 // TODO(jmesserly): need all standard List tests. |
| 295 |
| 296 test('observe length', () { |
| 297 var list = new ObservableList(); |
| 298 var notification = null; |
| 299 observe(() => list.length, (n) { notification = n; }); |
| 300 |
| 301 list.addAll([1, 2, 3]); |
| 302 expect(list, [1, 2, 3]); |
| 303 deliverChangesSync(); |
| 304 expect(notification, _change(0, 3), reason: 'addAll changes length'); |
| 305 |
| 306 list.add(4); |
| 307 expect(list, [1, 2, 3, 4]); |
| 308 deliverChangesSync(); |
| 309 expect(notification, _change(3, 4), reason: 'add changes length'); |
| 310 |
| 311 list.removeRange(1, 2); |
| 312 expect(list, [1, 4]); |
| 313 deliverChangesSync(); |
| 314 expect(notification, _change(4, 2), reason: 'removeRange changes length'); |
| 315 |
| 316 list.length = 5; |
| 317 expect(list, [1, 4, null, null, null]); |
| 318 deliverChangesSync(); |
| 319 expect(notification, _change(2, 5), reason: 'length= changes length'); |
| 320 notification = null; |
| 321 |
| 322 list[2] = 9000; |
| 323 expect(list, [1, 4, 9000, null, null]); |
| 324 deliverChangesSync(); |
| 325 expect(notification, null, reason: '[]= does not change length'); |
| 326 |
| 327 list.clear(); |
| 328 expect(list, []); |
| 329 deliverChangesSync(); |
| 330 expect(notification, _change(5, 0), reason: 'clear changes length'); |
| 331 }); |
| 332 |
| 333 test('observe index', () { |
| 334 var list = new ObservableList.from([1, 2, 3]); |
| 335 var notification = null; |
| 336 observe(() => list[1], (n) { notification = n; }); |
| 337 |
| 338 list.add(4); |
| 339 expect(list, [1, 2, 3, 4]); |
| 340 deliverChangesSync(); |
| 341 expect(notification, null, |
| 342 reason: 'add does not change existing items'); |
| 343 |
| 344 list[1] = 777; |
| 345 expect(list, [1, 777, 3, 4]); |
| 346 deliverChangesSync(); |
| 347 expect(notification, _change(2, 777)); |
| 348 |
| 349 notification = null; |
| 350 list[2] = 9000; |
| 351 expect(list, [1, 777, 9000, 4]); |
| 352 deliverChangesSync(); |
| 353 expect(notification, null, |
| 354 reason: 'setting a different index should not fire change'); |
| 355 |
| 356 list[1] = 44; |
| 357 list[1] = 43; |
| 358 list[1] = 42; |
| 359 expect(list, [1, 42, 9000, 4]); |
| 360 deliverChangesSync(); |
| 361 expect(notification, _change(777, 42)); |
| 362 |
| 363 notification = null; |
| 364 list.length = 2; |
| 365 expect(list, [1, 42]); |
| 366 deliverChangesSync(); |
| 367 expect(notification, null, |
| 368 reason: 'did not truncate the observed item'); |
| 369 |
| 370 list.length = 1; // truncate |
| 371 list.add(2); |
| 372 expect(list, [1, 2]); |
| 373 deliverChangesSync(); |
| 374 expect(notification, _change(42, 2), |
| 375 reason: 'item truncated and added back'); |
| 376 |
| 377 notification = null; |
| 378 list.length = 1; // truncate |
| 379 list.add(2); |
| 380 expect(list, [1, 2]); |
| 381 deliverChangesSync(); |
| 382 expect(notification, null, |
| 383 reason: 'truncated but added same item back'); |
| 384 }); |
| 385 |
| 386 test('toString', () { |
| 387 var list = new ObservableList.from([1, 2, 3]); |
| 388 var notification = null; |
| 389 observe(() => list.toString(), (n) { notification = n; }); |
| 390 list[2] = 4; |
| 391 deliverChangesSync(); |
| 392 expect(notification, _change('[1, 2, 3]', '[1, 2, 4]')); |
| 393 }); |
| 394 }); |
| 395 |
| 396 |
| 397 group('ObservableSet', () { |
| 398 // TODO(jmesserly): need all standard Set tests. |
| 399 |
| 400 test('observe length', () { |
| 401 var set = new ObservableSet(); |
| 402 var notification = null; |
| 403 observe(() => set.length, (n) { notification = n; }); |
| 404 |
| 405 set.addAll([1, 2, 3]); |
| 406 expect(set, [1, 2, 3]); |
| 407 deliverChangesSync(); |
| 408 expect(notification, _change(0, 3), reason: 'addAll changes length'); |
| 409 |
| 410 set.add(4); |
| 411 expect(set, [1, 2, 3, 4]); |
| 412 deliverChangesSync(); |
| 413 expect(notification, _change(3, 4), reason: 'add changes length'); |
| 414 |
| 415 set.removeAll([2, 3]); |
| 416 expect(set, [1, 4]); |
| 417 deliverChangesSync(); |
| 418 expect(notification, _change(4, 2), reason: 'removeAll changes length'); |
| 419 |
| 420 set.remove(1); |
| 421 expect(set, [4]); |
| 422 deliverChangesSync(); |
| 423 expect(notification, _change(2, 1), reason: 'remove changes length'); |
| 424 |
| 425 notification = null; |
| 426 set.add(4); |
| 427 expect(set, [4]); |
| 428 deliverChangesSync(); |
| 429 expect(notification, null, reason: 'item already exists'); |
| 430 |
| 431 set.clear(); |
| 432 expect(set, []); |
| 433 deliverChangesSync(); |
| 434 expect(notification, _change(1, 0), reason: 'clear changes length'); |
| 435 }); |
| 436 |
| 437 test('observe item', () { |
| 438 var set = new ObservableSet.from([1, 2, 3]); |
| 439 var notification = null; |
| 440 observe(() => set.contains(2), (n) { notification = n; }); |
| 441 |
| 442 set.add(4); |
| 443 expect(set, [1, 2, 3, 4]); |
| 444 deliverChangesSync(); |
| 445 expect(notification, null, reason: 'add does not change existing items'); |
| 446 |
| 447 set.remove(3); |
| 448 expect(set, [1, 2, 4]); |
| 449 expect(notification, null, |
| 450 reason: 'removing an item does not change other items'); |
| 451 |
| 452 set.remove(2); |
| 453 expect(set, [1, 4]); |
| 454 deliverChangesSync(); |
| 455 expect(notification, _change(true, false)); |
| 456 |
| 457 notification = null; |
| 458 set.removeAll([2, 3]); |
| 459 expect(set, [1, 4]); |
| 460 deliverChangesSync(); |
| 461 expect(notification, null, reason: 'item already removed'); |
| 462 |
| 463 set.add(2); |
| 464 expect(set, [1, 2, 4]); |
| 465 deliverChangesSync(); |
| 466 expect(notification, _change(false, true), reason: 'item added again'); |
| 467 }); |
| 468 |
| 469 test('toString', () { |
| 470 var original = new Set.from([1, 2, 3]); |
| 471 var set = new ObservableSet.from(original); |
| 472 var notification = null; |
| 473 observe(() => set.toString(), (n) { notification = n; }); |
| 474 set.add(4); |
| 475 deliverChangesSync(); |
| 476 var updated = new Set.from([1, 2, 3, 4]); |
| 477 |
| 478 // Note: using Set.toString as the exectation, so the order is the same |
| 479 // as with ObservableSet, regardless of how hashCode is implemented. |
| 480 expect(notification, _change('$original', '$updated')); |
| 481 }); |
| 482 }); |
| 483 |
| 484 |
| 485 group('ObservableMap', () { |
| 486 // TODO(jmesserly): need all standard Map tests. |
| 487 |
| 488 test('observe length', () { |
| 489 var map = new ObservableMap(); |
| 490 var notification = null; |
| 491 observe(() => map.length, (n) { notification = n; }); |
| 492 |
| 493 map['a'] = 1; |
| 494 map.putIfAbsent('b', () => 2); |
| 495 map['c'] = 3; |
| 496 expect(map, {'a': 1, 'b': 2, 'c': 3}); |
| 497 deliverChangesSync(); |
| 498 expect(notification, _change(0, 3), reason: 'adding changes length'); |
| 499 |
| 500 map['d'] = 4; |
| 501 expect(map, {'a': 1, 'b': 2, 'c': 3, 'd': 4}); |
| 502 deliverChangesSync(); |
| 503 expect(notification, _change(3, 4), reason: 'add changes length'); |
| 504 |
| 505 map.remove('b'); |
| 506 map.remove('c'); |
| 507 expect(map, {'a': 1, 'd': 4}); |
| 508 deliverChangesSync(); |
| 509 expect(notification, _change(4, 2), reason: 'removeRange changes length'); |
| 510 |
| 511 notification = null; |
| 512 map['d'] = 9000; |
| 513 expect(map, {'a': 1, 'd': 9000}); |
| 514 deliverChangesSync(); |
| 515 expect(notification, null, reason: 'update item does not change length'); |
| 516 |
| 517 map.clear(); |
| 518 expect(map, {}); |
| 519 deliverChangesSync(); |
| 520 expect(notification, _change(2, 0), reason: 'clear changes length'); |
| 521 }); |
| 522 |
| 523 test('observe index', () { |
| 524 var map = new ObservableMap.from({'a': 1, 'b': 2, 'c': 3}); |
| 525 var notification = null; |
| 526 observe(() => map['b'], (n) { notification = n; }); |
| 527 |
| 528 map.putIfAbsent('d', () => 4); |
| 529 expect(map, {'a': 1, 'b': 2, 'c': 3, 'd': 4}); |
| 530 deliverChangesSync(); |
| 531 expect(notification, null, reason: 'add does not change existing items'); |
| 532 |
| 533 map['b'] = null; |
| 534 expect(map, {'a': 1, 'b': null, 'c': 3, 'd': 4}); |
| 535 deliverChangesSync(); |
| 536 expect(notification, _change(2, null)); |
| 537 |
| 538 map['b'] = 777; |
| 539 expect(map, {'a': 1, 'b': 777, 'c': 3, 'd': 4}); |
| 540 deliverChangesSync(); |
| 541 expect(notification, _change(null, 777)); |
| 542 |
| 543 notification = null; |
| 544 map.putIfAbsent('b', () => 1234); |
| 545 expect(map, {'a': 1, 'b': 777, 'c': 3, 'd': 4}); |
| 546 deliverChangesSync(); |
| 547 expect(notification, null, reason: 'item already there'); |
| 548 |
| 549 map['c'] = 9000; |
| 550 expect(map, {'a': 1, 'b': 777, 'c': 9000, 'd': 4}); |
| 551 deliverChangesSync(); |
| 552 expect(notification, null, reason: 'setting a different item'); |
| 553 |
| 554 map['b'] = 44; |
| 555 map['b'] = 43; |
| 556 map['b'] = 42; |
| 557 expect(map, {'a': 1, 'b': 42, 'c': 9000, 'd': 4}); |
| 558 deliverChangesSync(); |
| 559 expect(notification, _change(777, 42)); |
| 560 |
| 561 notification = null; |
| 562 map.remove('a'); |
| 563 map.remove('d'); |
| 564 expect(map, {'b': 42, 'c': 9000}); |
| 565 deliverChangesSync(); |
| 566 expect(notification, null, reason: 'did not remove the observed item'); |
| 567 |
| 568 map.remove('b'); |
| 569 map['b'] = 2; |
| 570 expect(map, {'b': 2, 'c': 9000}); |
| 571 deliverChangesSync(); |
| 572 expect(notification, _change(42, 2), reason: 'removed and added back'); |
| 573 }); |
| 574 |
| 575 test('toString', () { |
| 576 var map = new ObservableMap.from({'a': 1, 'b': 2}, |
| 577 createMap: () => new LinkedHashMap()); |
| 578 |
| 579 var notification = null; |
| 580 observe(() => map.toString(), (n) { notification = n; }); |
| 581 map.remove('b'); |
| 582 map['c'] = 3; |
| 583 deliverChangesSync(); |
| 584 |
| 585 expect(notification, _change('{a: 1, b: 2}', '{a: 1, c: 3}')); |
| 586 }); |
| 587 }); |
| 588 |
| 589 } |
| 590 |
| 591 _change(oldValue, newValue) => new ChangeNotification(oldValue, newValue); |
| 592 |
| 593 /** |
| 594 * This is similar to ObservableReference, but with fields public for testing. |
| 595 */ |
| 596 class TestObservable<T> { |
| 597 var observers; |
| 598 T rawValue; |
| 599 |
| 600 TestObservable([T initialValue]) : rawValue = initialValue; |
| 601 |
| 602 T get value { |
| 603 if (observeReads) observers = notifyRead(observers); |
| 604 return rawValue; |
| 605 } |
| 606 |
| 607 void set value(T newValue) { |
| 608 if (observers != null) observers = notifyWrite(observers); |
| 609 rawValue = newValue; |
| 610 } |
| 611 } |
OLD | NEW |