1 // ========================================================================== 2 // Project: SproutCore Costello - Property Observing Library 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 /*global module, test, ok, isObj, equals, expects */ 8 9 10 var enumerables; // global variables 11 var DummyEnumerable = SC.Object.extend(SC.Enumerable, { 12 13 content: [], 14 15 length: function () { return this.content.length; }.property(), 16 17 objectAt: function (idx) { return this.content[idx]; }, 18 19 nextObject: function (idx) { return this.content[idx]; }, 20 21 // add support for reduced properties. 22 unknownProperty: function (key, value) { 23 var ret = this.reducedProperty(key, value); 24 if (ret === undefined) { 25 if (value !== undefined) this[key] = value; 26 ret = value; 27 } 28 return ret; 29 }, 30 31 replace: function (start, removed, added) { 32 var ret = this.content.replace(start, removed, added), 33 addedLength = added ? added.length : 0; 34 35 this.enumerableContentDidChange(start, addedLength, addedLength - removed); 36 return ret; 37 }, 38 39 unshiftObject: function (object) { 40 this.replace(0, 0, [object]); 41 return object; 42 }, 43 44 shiftObject: function () { 45 var ret = this.replace(0, 1); 46 return ret; 47 }, 48 49 pushObject: function (object) { 50 this.replace(this.content.length - 1, 0, [object]); 51 return object; 52 }, 53 54 popObject: function () { 55 var ret = this.replace(this.content.length - 1, 1); 56 return ret; 57 } 58 59 }); 60 61 var runFunc = function (a, b) { return ['DONE', a, b]; }; 62 var invokeWhileOK = function () { return "OK"; }; 63 var invokeWhileNotOK = function () { return "FAIL"; }; 64 var reduceTestFunc = function (prev, item, idx, e, pname) { return pname || 'TEST'; }; 65 66 var CommonArray = [ 67 { 68 first: "Charles", 69 gender: "male", 70 californian: NO, 71 ready: YES, 72 visited: "Prague", 73 doneTravelling: NO, 74 run: runFunc, 75 invokeWhileTest: invokeWhileOK, 76 balance: 1 77 }, 78 79 { 80 first: "Jenna", 81 gender: "female", 82 californian: YES, 83 ready: YES, 84 visited: "Prague", 85 doneTravelling: NO, 86 run: runFunc, 87 invokeWhileTest: invokeWhileOK, 88 balance: 2 89 }, 90 91 { 92 first: "Peter", 93 gender: "male", 94 californian: NO, 95 ready: YES, 96 visited: "Prague", 97 doneTravelling: NO, 98 run: runFunc, 99 invokeWhileTest: invokeWhileNotOK, 100 balance: 3 101 }, 102 103 { 104 first: "Chris", 105 gender: "male", 106 californian: NO, 107 ready: YES, 108 visited: "Prague", 109 doneTravelling: NO, 110 run: runFunc, 111 invokeWhileTest: invokeWhileOK, 112 balance: 4 113 } 114 ]; 115 116 module("Real Array & DummyEnumerable", { 117 118 setup: function () { 119 enumerables = [SC.$A(CommonArray), DummyEnumerable.create({ content: SC.clone(CommonArray) })]; 120 }, 121 122 teardown: function () { 123 enumerables = null; 124 delete Array.prototype["@max(balance)"]; // remove cached value 125 delete Array.prototype["@min(balance)"]; 126 } 127 128 }); 129 130 test("should get enumerator that iterates through objects", function () { 131 var src, ary2 = enumerables; 132 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 133 src = ary2[idx2]; 134 var e = src.enumerator(); 135 ok(e !== null, 'enumerator must not be null'); 136 137 var idx = 0; 138 var cur; 139 while(cur = e.nextObject()) { 140 equals(src.objectAt(idx), cur, "object at index %@".fmt(idx)); 141 idx++; 142 } 143 144 equals(src.get('length'), idx); 145 } 146 }); 147 148 test("should return firstObject for item with content", function () { 149 var src, ary2 = enumerables; 150 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 151 src = ary2[idx2]; 152 equals(src.firstObject(), CommonArray[0], 'firstObject should return first object'); 153 } 154 155 equals([].firstObject(), undefined, 'firstObject() on empty enumerable should return undefined'); 156 }); 157 158 test("should run forEach() to go through objects", function () { 159 var src, ary2 = enumerables; 160 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 161 src = ary2[idx2]; 162 var idx = 0; 163 164 // save for testing later 165 var items = []; 166 var indexes = []; 167 var arrays = []; 168 var targets = []; 169 170 src.forEach(function (item, index, array) { 171 items.push(item); 172 indexes.push(index); 173 arrays.push(array); 174 targets.push(this); 175 }, this); 176 177 var len = src.get('length'); 178 for(idx=0;idx<len;idx++) { 179 equals(items[idx], src.objectAt(idx)); 180 equals(indexes[idx], idx); 181 equals(arrays[idx], src); 182 183 // use this method because equals() is taking too much time to log out 184 // results. probably an issue with jsDump 185 ok(targets[idx] === this, 'target should always be this'); 186 } 187 } 188 }); 189 190 test("should map to values while passing proper params", function () { 191 var src, ary2 = enumerables; 192 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 193 src = ary2[idx2]; 194 var idx = 0; 195 196 // save for testing later 197 var items = []; 198 var indexes = []; 199 var arrays = []; 200 var targets = []; 201 202 var mapped = src.map(function (item, index, array) { 203 items.push(item); 204 indexes.push(index); 205 arrays.push(array); 206 targets.push(this); 207 208 return index; 209 }, this); 210 211 var len = src.get('length'); 212 for(idx=0;idx<len;idx++) { 213 equals(src.objectAt(idx), items[idx], "items"); 214 equals(idx, indexes[idx], "indexes"); 215 equals(src, arrays[idx], 'arrays'); 216 equals(SC.guidFor(this), SC.guidFor(targets[idx]), "this"); 217 218 equals(idx, mapped[idx], "mapped"); 219 } 220 } 221 }); 222 223 test("should filter to items that return for callback", function () { 224 var src, ary2 = enumerables; 225 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 226 src = ary2[idx2]; 227 var idx = 0; 228 229 // save for testing later 230 var items = []; 231 var indexes = []; 232 var arrays = []; 233 var targets = []; 234 235 var filtered = src.filter(function (item, index, array) { 236 items.push(item); 237 indexes.push(index); 238 arrays.push(array); 239 targets.push(this); 240 241 return item.gender === "female"; 242 }, this); 243 244 var len = src.get('length'); 245 for(idx=0;idx<len;idx++) { 246 equals(src.objectAt(idx), items[idx], "items"); 247 equals(idx, indexes[idx], "indexes"); 248 equals(src, arrays[idx], 'arrays'); 249 equals(SC.guidFor(this), SC.guidFor(targets[idx]), "this"); 250 } 251 252 equals(filtered.length, 1); 253 equals(filtered[0].first, "Jenna"); 254 } 255 }); 256 257 test("should return true if function for every() returns true", function () { 258 var src, ary2 = enumerables; 259 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 260 src = ary2[idx2]; 261 var idx = 0; 262 263 // save for testing later 264 var items = []; 265 var indexes = []; 266 var arrays = []; 267 var targets = []; 268 269 var result = src.every(function (item, index, array) { 270 items.push(item); 271 indexes.push(index); 272 arrays.push(array); 273 targets.push(this); 274 275 return true; 276 }, this); 277 278 var len = src.get('length'); 279 for(idx=0;idx<len;idx++) { 280 equals(src.objectAt(idx), items[idx], "items"); 281 equals(idx, indexes[idx], "indexes"); 282 equals(src, arrays[idx], 'arrays'); 283 equals(SC.guidFor(this), SC.guidFor(targets[idx]), "this"); 284 } 285 286 equals(result, YES); 287 } 288 }); 289 290 test("should return false if one function for every() returns false", function () { 291 var src, ary2 = enumerables; 292 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 293 src = ary2[idx2]; 294 var result = src.every(function (item, index, array) { 295 return item.gender === "male"; 296 }, this); 297 equals(result, NO); 298 } 299 }); 300 301 test("should return false if all functions for some() returns false", function () { 302 var src, ary2 = enumerables; 303 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 304 src = ary2[idx2]; 305 var idx = 0; 306 307 // save for testing later 308 var items = []; 309 var indexes = []; 310 var arrays = []; 311 var targets = []; 312 313 var result = src.some(function (item, index, array) { 314 items.push(item); 315 indexes.push(index); 316 arrays.push(array); 317 targets.push(this); 318 319 return false; 320 }, this); 321 322 var len = src.get('length'); 323 for(idx=0;idx<len;idx++) { 324 equals(src.objectAt(idx), items[idx], "items"); 325 equals(idx, indexes[idx], "indexes"); 326 equals(src, arrays[idx], 'arrays'); 327 equals(SC.guidFor(this), SC.guidFor(targets[idx]), "this"); 328 } 329 330 equals(result, NO); 331 } 332 }); 333 334 test("should return true if one function for some() returns true", function () { 335 var src, ary2 = enumerables; 336 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 337 src = ary2[idx2]; 338 var result = src.some(function (item, index, array) { 339 return item.gender !== "male"; 340 }, this); 341 equals(result, YES); 342 } 343 }); 344 345 test("should mapProperty for all items", function () { 346 var src, ary2 = enumerables; 347 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 348 src = ary2[idx2]; 349 var mapped = src.mapProperty("first"); 350 var idx; 351 var len = src.get('length'); 352 for(idx=0;idx<len;idx++) { 353 equals(mapped[idx], src.objectAt(idx).first); 354 } 355 } 356 }); 357 358 test("should filterProperty with match", function () { 359 var src, ary2 = enumerables; 360 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 361 src = ary2[idx2]; 362 var filtered = src.filterProperty("gender", "female"); 363 equals(filtered.length, 1); 364 equals(filtered[0].first, "Jenna"); 365 } 366 }); 367 368 test("should filterProperty with default bool", function () { 369 var src, ary2 = enumerables; 370 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 371 src = ary2[idx2]; 372 var filtered = src.filterProperty("californian"); 373 equals(filtered.length, 1); 374 equals(filtered[0].first, "Jenna"); 375 } 376 }); 377 378 test("should groupBy a given property", function () { 379 var src, ary2 = enumerables; 380 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 381 src = ary2[idx2]; 382 var filtered = src.groupBy("gender"); 383 equals(filtered.length, 2); 384 equals(filtered[1][0].first, "Jenna"); 385 } 386 }); 387 388 389 test("everyProperty should return true if all properties macth", function () { 390 var src, ary2 = enumerables; 391 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 392 src = ary2[idx2]; 393 var ret = src.everyProperty('visited', 'Prague'); 394 equals(YES, ret, "visited"); 395 } 396 }); 397 398 test("everyProperty should return true if all properties true", function () { 399 var src, ary2 = enumerables; 400 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 401 src = ary2[idx2]; 402 var ret = src.everyProperty('ready'); 403 equals(YES, ret, "ready"); 404 } 405 }); 406 407 test("everyProperty should return false if any properties false", function () { 408 var src, ary2 = enumerables; 409 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 410 src = ary2[idx2]; 411 var ret = src.everyProperty('gender', 'male'); 412 equals(NO, ret, "ready"); 413 } 414 }); 415 416 test("someProperty should return false if all properties not match", function () { 417 var src, ary2 = enumerables; 418 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 419 src = ary2[idx2]; 420 var ret = src.someProperty('visited', 'Timbuktu'); 421 equals(NO, ret, "visited"); 422 } 423 }); 424 425 test("someProperty should return false if all properties false", function () { 426 var src, ary2 = enumerables; 427 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 428 src = ary2[idx2]; 429 var ret = src.someProperty('doneTravelling'); 430 equals(NO, ret, "doneTravelling"); 431 } 432 }); 433 434 test("someProperty should return true if any properties true", function () { 435 var src, ary2 = enumerables; 436 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 437 src = ary2[idx2]; 438 var ret = src.someProperty('first', 'Charles'); 439 equals(YES, ret, "first"); 440 } 441 }); 442 443 test("invokeWhile should call method on member objects until return does not match", function () { 444 var src, ary2 = enumerables; 445 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 446 src = ary2[idx2]; 447 var ret = src.invokeWhile("OK", "invokeWhileTest", "item2"); 448 equals("FAIL", ret, "return value"); 449 } 450 }); 451 452 test("get @min(balance) should return the minimum balance", function () { 453 var src, ary2 = enumerables; 454 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 455 src = ary2[idx2]; 456 equals(1, src.get('@min(balance)')); 457 } 458 }); 459 460 test("get @max(balance) should return the maximum balance", function () { 461 var src, ary2 = enumerables; 462 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 463 src = ary2[idx2]; 464 equals(4, src.get('@max(balance)')); 465 } 466 }); 467 468 test("get @minObject(balance) should return the record with min balance", function () { 469 var src, ary2 = enumerables; 470 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 471 src = ary2[idx2]; 472 equals(src.objectAt(0), src.get('@minObject(balance)')); 473 } 474 }); 475 476 test("get @maxObject(balance) should return the record with the max balance", function () { 477 var src, ary2 = enumerables; 478 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 479 src = ary2[idx2]; 480 equals(src.objectAt(3), src.get('@maxObject(balance)')); 481 } 482 }); 483 484 test("get @sum(balance) should return the sum of the balances.", function () { 485 var src, ary2 = enumerables; 486 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 487 src = ary2[idx2]; 488 equals(1+2+3+4, src.get("@sum(balance)")); 489 } 490 }); 491 492 test("get @average(balance) should return the average of balances", function () { 493 var src, ary2 = enumerables; 494 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 495 src = ary2[idx2]; 496 equals((1+2+3+4)/4, src.get("@average(balance)")); 497 } 498 }); 499 500 test("should invoke custom reducer", function () { 501 var src, ary2 = enumerables; 502 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 503 src = ary2[idx2]; 504 // install reducer method 505 src.reduceTest = reduceTestFunc; 506 equals("TEST", src.get("@test")); 507 equals("prop", src.get("@test(prop)")); 508 } 509 }); 510 511 test("Should trigger observer on lastObject property when it changes", function () { 512 513 // Perform tests on each sample enumerable in enumerables. 514 for (var i = 0, len = enumerables.length; i < len; i++) { 515 var enumerable = enumerables[i], 516 enumerableLength = enumerable.get('length'), 517 callCount = 0, 518 testObject = { 519 first: "John", 520 }; 521 522 // Observe the enumerable for updates to `lastObject`. 523 enumerable.addObserver("lastObject", function () { 524 callCount++; 525 }); 526 527 // Inserting an item in the middle doesn't change lastObject. 528 enumerable.replace(1, 0, [testObject]); 529 equals(callCount, 0, "The lastObject observer should have fired this many times (replace on enumerable %@)".fmt(i + 1)); 530 531 // Removing an item in the middle doesn't change lastObject. 532 enumerable.replace(1, 1); 533 equals(callCount, 0, "The lastObject observer should have fired this many times (replace on enumerable %@)".fmt(i + 1)); 534 535 // Shifting an item to the front doesn't change lastObject. 536 enumerable.shiftObject(testObject); 537 equals(callCount, 0, "The lastObject observer should have fired this many times (shiftObject on enumerable %@)".fmt(i + 1)); 538 539 // Unshifting an item from the front doesn't change lastObject. 540 enumerable.unshiftObject(testObject); 541 equals(callCount, 0, "The lastObject observer should have fired this many times (unshiftObject on enumerable %@)".fmt(i + 1)); 542 543 // Appending an item to the end changes the lastObject. 544 enumerable.pushObject(testObject); 545 equals(callCount, 1, "The lastObject observer should have fired this many times (pushObject on enumerable %@)".fmt(i + 1)); 546 547 // Popping an item from the end changes the lastObject. 548 enumerable.popObject(); 549 equals(callCount, 2, "The lastObject observer should have fired this many times (popObject on enumerable %@)".fmt(i + 1)); 550 551 // Replacing only the last item changes the lastObject. 552 enumerable.replace(enumerable.get('length') - 1, 1, [testObject]); 553 equals(callCount, 3, "The lastObject observer should have fired this many times (replace on enumerable %@)".fmt(i + 1)); 554 555 // Replacing the last two items with one greater number changes the lastObject. 556 enumerable.replace(enumerable.get('length') - 2, 2, [testObject, testObject, testObject]); 557 equals(callCount, 4, "The lastObject observer should have fired this many times (replace on enumerable %@)".fmt(i + 1)); 558 559 // Replacing the last two items with same number changes the lastObject. 560 enumerable.replace(enumerable.get('length') - 2, 2, [testObject, testObject]); 561 equals(callCount, 5, "The lastObject observer should have fired this many times (replace on enumerable %@)".fmt(i + 1)); 562 563 // Replacing the last two items with one fewer number changes the lastObject. 564 enumerable.replace(enumerable.get('length') - 2, 2, [testObject]); 565 equals(callCount, 6, "The lastObject observer should have fired this many times (replace on enumerable %@)".fmt(i + 1)); 566 567 // Replacing the last two items with two fewer number changes the lastObject. 568 enumerable.replace(enumerable.get('length') - 2, 2); 569 equals(callCount, 7, "The lastObject observer should have fired this many times (replace on enumerable %@)".fmt(i + 1)); 570 } 571 }); 572 573 test("should trigger observer on property when firstObject changes", function () { 574 var src, ary2 = enumerables; 575 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 576 src = ary2[idx2]; 577 578 var callCount = 0; 579 src.addObserver("firstObject", function () { 580 callCount++; 581 }); 582 583 src.shiftObject(); 584 585 equals(callCount, 1, "callCount"); 586 } 587 }); 588 589 test("should trigger observer of reduced prop when array changes once property retrieved once", function () { 590 var src, ary2 = enumerables; 591 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 592 src = ary2[idx2]; 593 // get the property...this will install the reducer property... 594 src.get("@max(balance)"); 595 596 // install observer 597 var observedValue = null; 598 src.addObserver("@max(balance)", function () { 599 observedValue = src.get("@max(balance)"); 600 }); 601 602 //src.addProbe('[]'); 603 //src.addProbe('@max(balance)'); 604 605 // add record to array 606 src.pushObject({ 607 first: "John", 608 gender: "male", 609 californian: NO, 610 ready: YES, 611 visited: "Paris", 612 balance: 5 613 }); 614 615 //SC.NotificationQueue.flush(); // force observers to trigger 616 617 // observed value should now be set because the reduced property observer 618 // was triggered when we changed the array contents. 619 equals(5, observedValue, "observedValue"); 620 } 621 }); 622 623 624 test("should trigger observer of reduced prop when array changes - even if you never retrieved the property before", function () { 625 var src, ary2 = enumerables; 626 for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) { 627 src = ary2[idx2]; 628 // install observer 629 var observedValue = null; 630 src.addObserver("@max(balance)", function () { 631 observedValue = src.get("@max(balance)"); 632 }); 633 634 // add record to array 635 src.pushObject({ 636 first: "John", 637 gender: "male", 638 californian: NO, 639 ready: YES, 640 visited: "Paris", 641 balance: 5 642 }); 643 644 //SC.NotificationQueue.flush(); // force observers to trigger 645 646 // observed value should now be set because the reduced property observer 647 // was triggered when we changed the array contents. 648 equals(5, observedValue, "observedValue"); 649 } 650 }); 651 652 test("should find the first element matching the criteria", function () { 653 var people = enumerables[1]; 654 var jenna = people.find(function (person) { return person.gender == 'female'; }); 655 equals(jenna.first, 'Jenna'); 656 }); 657 658 var source; // global variables 659 660 module("Real Array", { 661 662 setup: function () { 663 source = SC.$A(CommonArray); 664 }, 665 666 teardown: function () { 667 delete source; 668 669 delete Array.prototype["@max(balance)"]; // remove cached value 670 delete Array.prototype["@min(balance)"]; 671 } 672 673 }); 674 675 /* 676 This is a particular problem because reduced properties are registered 677 as dependent keys, which are not automatically configured in native 678 Arrays (where the SC.Object.init method is not run). 679 680 The fix for this problem was to add an initObservable() method to 681 SC.Observable that will configure bindings and dependent keys. This 682 method is called from SC.Object.init() and it is called in 683 SC.Observable._notifyPropertyChanges if it has not been called already. 684 685 SC.Enumerable was in turn modified to register reducers as dependent 686 keys so that now they will be registered on the Array before any 687 property change notifications are sent. 688 */ 689 test("should notify observers even if reduced property is cached on prototype", function () { 690 // make sure reduced property is cached 691 source.get("@max(balance)"); 692 693 // now make a clone and observe 694 source = SC.$A(CommonArray); 695 696 // get the property...this will install the reducer property... 697 source.get("@max(balance)"); 698 699 // install observer 700 var observedValue = null; 701 source.addObserver("@max(balance)", function () { 702 observedValue = source.get("@max(balance)"); 703 }); 704 705 //source.addProbe('[]'); 706 //source.addProbe('@max(balance)'); 707 708 // add record to array 709 source.pushObject({ 710 first: "John", 711 gender: "male", 712 californian: NO, 713 ready: YES, 714 visited: "Paris", 715 balance: 5 716 }); 717 718 //SC.NotificationQueue.flush(); // force observers to trigger 719 720 // observed value should now be set because the reduced property observer 721 // was triggered when we changed the array contents. 722 equals(5, observedValue, "observedValue"); 723 }); 724