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